Stop data sync after account authorization revoke
This commit is contained in:
@@ -190,24 +190,42 @@ public sealed class RemoteControlClient : IDisposable
|
||||
cancellationToken, RemoteDataProtocol.MaxBatchBytes).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<RemoteSyncBatch>> FlushDataBatchesAsync(
|
||||
RemoteDataBatchQueue queue,
|
||||
ReportingConfig reportingConfig,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
FlushDataBatchesAsync(queue, reportingConfig, new HashSet<string>(StringComparer.Ordinal), cancellationToken);
|
||||
|
||||
public async Task<IReadOnlyList<RemoteSyncBatch>> FlushDataBatchesAsync(
|
||||
RemoteDataBatchQueue queue,
|
||||
ReportingConfig reportingConfig,
|
||||
ISet<string> blockedAccountIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
var confirmed = new List<RemoteSyncBatch>();
|
||||
foreach (var batch in queue.Pending())
|
||||
{
|
||||
if (blockedAccountIds.Contains(batch.AccountId))
|
||||
continue;
|
||||
var filtered = RemoteDataBatchAuthorization.Filter(reportingConfig, batch, out _);
|
||||
if (filtered is null)
|
||||
{
|
||||
queue.Drop(batch.BatchId);
|
||||
continue;
|
||||
}
|
||||
var acknowledgement = await SubmitDataBatchAsync(reportingConfig, filtered, cancellationToken).ConfigureAwait(false);
|
||||
if (acknowledgement.Accepted && queue.MarkConfirmed(acknowledgement))
|
||||
confirmed.Add(filtered);
|
||||
try
|
||||
{
|
||||
var acknowledgement = await SubmitDataBatchAsync(reportingConfig, filtered, cancellationToken).ConfigureAwait(false);
|
||||
if (acknowledgement.Accepted && queue.MarkConfirmed(acknowledgement))
|
||||
confirmed.Add(filtered);
|
||||
}
|
||||
catch (RemoteClientException exception) when (exception.StatusCode == 403 && exception.Code == "AccountNotAuthorized")
|
||||
{
|
||||
// Authorization revocation is terminal for locally buffered content: do not retain or retry it.
|
||||
queue.DropAllForAccount(batch.AccountId);
|
||||
blockedAccountIds.Add(batch.AccountId);
|
||||
}
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
@@ -362,6 +362,17 @@ public sealed class RemoteDataBatchQueue
|
||||
}
|
||||
}
|
||||
|
||||
public int DropAllForAccount(string accountId)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
var removed = state.Items.RemoveAll(item => string.Equals(item.AccountId, accountId, StringComparison.Ordinal));
|
||||
if (removed > 0)
|
||||
SaveLocked();
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MarkConfirmed(RemoteDataBatchAck acknowledgement)
|
||||
{
|
||||
lock (gate)
|
||||
|
||||
@@ -48,6 +48,7 @@ public sealed class RemoteAgentHostedService(
|
||||
var syncState = options.EnableDataSync
|
||||
? new RemoteDataSyncStateStore(Path.Combine(options.DataDirectory, "remote-data-sync-state.json"))
|
||||
: null;
|
||||
var blockedDataSyncAccounts = new HashSet<string>(StringComparer.Ordinal);
|
||||
var lastDataSyncAt = DateTimeOffset.MinValue;
|
||||
string? registeredActiveAccountId = null;
|
||||
bool? registeredActiveAccountVerified = null;
|
||||
@@ -78,6 +79,7 @@ public sealed class RemoteAgentHostedService(
|
||||
registeredActiveAccountId = snapshot.ActiveAccountId;
|
||||
registeredActiveAccountVerified = snapshot.ActiveAccountVerified;
|
||||
registeredReportingConfigVersion = reporting.ConfigVersion;
|
||||
blockedDataSyncAccounts.Clear();
|
||||
retry = RetryDelay;
|
||||
}
|
||||
|
||||
@@ -87,20 +89,24 @@ public sealed class RemoteAgentHostedService(
|
||||
if (dataQueue is not null && syncState is not null && dataCollector is not null
|
||||
&& DateTimeOffset.UtcNow - lastDataSyncAt >= TimeSpan.FromSeconds(options.DataSyncIntervalSeconds))
|
||||
{
|
||||
await ReconcileDataStatusAsync(client, reporting, dataQueue, syncState, stoppingToken);
|
||||
await ReconcileDataStatusAsync(client, reporting, dataQueue, syncState, blockedDataSyncAccounts, stoppingToken);
|
||||
var blockedBeforeFlush = blockedDataSyncAccounts.ToHashSet(StringComparer.Ordinal);
|
||||
try
|
||||
{
|
||||
var confirmedBatches = await client.FlushDataBatchesAsync(dataQueue, reporting, stoppingToken);
|
||||
var confirmedBatches = await client.FlushDataBatchesAsync(dataQueue, reporting, blockedDataSyncAccounts, stoppingToken);
|
||||
foreach (var confirmedBatch in confirmedBatches)
|
||||
syncState.MarkConfirmed(confirmedBatch);
|
||||
foreach (var accountId in blockedDataSyncAccounts.Except(blockedBeforeFlush, StringComparer.Ordinal))
|
||||
logger.LogWarning("Data sync authorization was revoked; accountId={AccountId}; pending content was discarded and collection is paused.", accountId);
|
||||
}
|
||||
catch (Exception exception) when (exception is HttpRequestException or RemoteClientException)
|
||||
{
|
||||
logger.LogWarning("Data sync delivery deferred; type={ExceptionType}.", exception.GetType().Name);
|
||||
}
|
||||
// Collection must continue while the platform is unreachable; the durable queue is
|
||||
// the offline buffer and will be flushed on the next successful connection.
|
||||
await CollectDataBatchesAsync(remote, reporting, dataCollector, dataQueue, syncState, stoppingToken);
|
||||
// the offline buffer and will be flushed on the next successful connection. Revoked
|
||||
// accounts are the exception: their pending content is discarded and collection stops.
|
||||
await CollectDataBatchesAsync(remote, reporting, dataCollector, dataQueue, syncState, blockedDataSyncAccounts, stoppingToken);
|
||||
lastDataSyncAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
var pollAccountIds = reporting.Accounts
|
||||
@@ -133,14 +139,14 @@ public sealed class RemoteAgentHostedService(
|
||||
{
|
||||
logger.LogWarning("Remote control-plane request failed; code={Code}; status={StatusCode}; correlationId={CorrelationId}.",
|
||||
exception.Code, exception.StatusCode, exception.CorrelationId);
|
||||
await TryCollectOfflineDataBatchesAsync(remote, activeReporting, dataCollector, dataQueue, syncState, stoppingToken);
|
||||
await TryCollectOfflineDataBatchesAsync(remote, activeReporting, dataCollector, dataQueue, syncState, blockedDataSyncAccounts, stoppingToken);
|
||||
await DelayAsync(retry, stoppingToken);
|
||||
retry = TimeSpan.FromSeconds(Math.Min(retry.TotalSeconds * 2, 30));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogWarning("Remote agent cycle failed; type={ExceptionType}.", exception.GetType().Name);
|
||||
await TryCollectOfflineDataBatchesAsync(remote, activeReporting, dataCollector, dataQueue, syncState, stoppingToken);
|
||||
await TryCollectOfflineDataBatchesAsync(remote, activeReporting, dataCollector, dataQueue, syncState, blockedDataSyncAccounts, stoppingToken);
|
||||
await DelayAsync(retry, stoppingToken);
|
||||
retry = TimeSpan.FromSeconds(Math.Min(retry.TotalSeconds * 2, 30));
|
||||
}
|
||||
@@ -153,11 +159,14 @@ public sealed class RemoteAgentHostedService(
|
||||
IRemoteDataCollector collector,
|
||||
RemoteDataBatchQueue queue,
|
||||
RemoteDataSyncStateStore state,
|
||||
ISet<string> blockedAccountIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var account in reporting.Accounts.Where(item => item.Enabled))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (blockedAccountIds.Contains(account.AccountId))
|
||||
continue;
|
||||
var scopes = AuthorizedChatScopes(reporting, account.AccountId);
|
||||
if (scopes.Count == 0 || queue.Pending().Any(batch => string.Equals(batch.AccountId, account.AccountId, StringComparison.Ordinal)
|
||||
&& string.Equals(batch.StreamKey, "messages", StringComparison.Ordinal)))
|
||||
@@ -191,13 +200,14 @@ public sealed class RemoteAgentHostedService(
|
||||
IRemoteDataCollector? collector,
|
||||
RemoteDataBatchQueue? queue,
|
||||
RemoteDataSyncStateStore? state,
|
||||
ISet<string> blockedAccountIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (reporting is null || collector is null || queue is null || state is null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
await CollectDataBatchesAsync(remote, reporting, collector, queue, state, cancellationToken).ConfigureAwait(false);
|
||||
await CollectDataBatchesAsync(remote, reporting, collector, queue, state, blockedAccountIds, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -214,6 +224,7 @@ public sealed class RemoteAgentHostedService(
|
||||
ReportingConfig reporting,
|
||||
RemoteDataBatchQueue queue,
|
||||
RemoteDataSyncStateStore state,
|
||||
ISet<string> blockedAccountIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var account in reporting.Accounts.Where(item => item.Enabled))
|
||||
@@ -229,12 +240,23 @@ public sealed class RemoteAgentHostedService(
|
||||
// Older control planes do not expose the reconciliation endpoint; retain the legacy ACK path.
|
||||
continue;
|
||||
}
|
||||
catch (RemoteClientException exception) when (exception.StatusCode == 403 && exception.Code == "AccountNotAuthorized")
|
||||
{
|
||||
var droppedOnRevoke = queue.DropAllForAccount(account.AccountId);
|
||||
blockedAccountIds.Add(account.AccountId);
|
||||
logger.LogWarning(
|
||||
"Data sync authorization is revoked; accountId={AccountId}; pending content was discarded and collection is paused; droppedPendingBatches={DroppedPendingBatches}.",
|
||||
account.AccountId, droppedOnRevoke);
|
||||
continue;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
// A disconnected platform must not prevent local DB collection.
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockedAccountIds.Remove(account.AccountId))
|
||||
logger.LogInformation("Data sync authorization was restored; accountId={AccountId}; collection is resumed.", account.AccountId);
|
||||
if (!state.Reconcile(remote, account.AccountId, "messages"))
|
||||
continue;
|
||||
var dropped = queue.DropForAccount(account.AccountId, "messages");
|
||||
|
||||
Reference in New Issue
Block a user