Effects client reconciles pending effects: drop stale wait deadlines and seen keys, emit completion for vanished runs; run_not_active is terminal
This commit is contained in:
@@ -21,7 +21,7 @@ public sealed class EffectsService
|
|||||||
|
|
||||||
private readonly RudderClient _client;
|
private readonly RudderClient _client;
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
private readonly HashSet<(string RunId, string NodeId)> _seen = new();
|
private readonly Dictionary<(string RunId, string NodeId), string> _seen = new();
|
||||||
private readonly Dictionary<(string RunId, string NodeId), DateTimeOffset> _waitDeadlines = new();
|
private readonly Dictionary<(string RunId, string NodeId), DateTimeOffset> _waitDeadlines = new();
|
||||||
|
|
||||||
private bool _refreshDue;
|
private bool _refreshDue;
|
||||||
@@ -99,8 +99,9 @@ public sealed class EffectsService
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var key = (effect.RunId, effect.NodeId);
|
var key = (effect.RunId, effect.NodeId);
|
||||||
if (!_seen.Add(key))
|
if (_seen.ContainsKey(key))
|
||||||
continue;
|
continue;
|
||||||
|
_seen[key] = effect.ScenarioId;
|
||||||
|
|
||||||
if (string.Equals(effect.Type, EffectTypes.Wait, StringComparison.Ordinal))
|
if (string.Equals(effect.Type, EffectTypes.Wait, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -215,7 +216,12 @@ public sealed class EffectsService
|
|||||||
"/sdk/v1/scenarios/pending",
|
"/sdk/v1/scenarios/pending",
|
||||||
CancellationToken.None).ConfigureAwait(false);
|
CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
Ingest(response?.Effects);
|
if (response != null)
|
||||||
|
{
|
||||||
|
Ingest(response.Effects);
|
||||||
|
Reconcile(response.Effects);
|
||||||
|
}
|
||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
_nextHeartbeat = _client.Clock.UtcNow + HeartbeatInterval;
|
_nextHeartbeat = _client.Clock.UtcNow + HeartbeatInterval;
|
||||||
}
|
}
|
||||||
@@ -280,7 +286,7 @@ public sealed class EffectsService
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
var toRemove = new List<(string RunId, string NodeId)>();
|
var toRemove = new List<(string RunId, string NodeId)>();
|
||||||
foreach (var key in _seen)
|
foreach (var key in _seen.Keys)
|
||||||
{
|
{
|
||||||
if (key.RunId == runId)
|
if (key.RunId == runId)
|
||||||
toRemove.Add(key);
|
toRemove.Add(key);
|
||||||
@@ -296,6 +302,45 @@ public sealed class EffectsService
|
|||||||
Emit(OnScenarioFailed, new ScenarioFailedEffect(runId, scenarioId, nodeId, exception));
|
Emit(OnScenarioFailed, new ScenarioFailedEffect(runId, scenarioId, nodeId, exception));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Reconcile(IEnumerable<PendingEffect>? effects)
|
||||||
|
{
|
||||||
|
var incoming = new HashSet<(string RunId, string NodeId)>();
|
||||||
|
if (effects != null)
|
||||||
|
{
|
||||||
|
foreach (var effect in effects)
|
||||||
|
{
|
||||||
|
if (effect == null || string.IsNullOrEmpty(effect.RunId) || string.IsNullOrEmpty(effect.NodeId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
incoming.Add((effect.RunId, effect.NodeId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var finished = new Dictionary<string, string>();
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var stale = new List<(string RunId, string NodeId)>();
|
||||||
|
foreach (var key in _seen.Keys)
|
||||||
|
{
|
||||||
|
if (!incoming.Contains(key))
|
||||||
|
stale.Add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in stale)
|
||||||
|
{
|
||||||
|
finished[key.RunId] = _seen[key];
|
||||||
|
_seen.Remove(key);
|
||||||
|
_waitDeadlines.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in _seen.Keys)
|
||||||
|
finished.Remove(key.RunId);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in finished)
|
||||||
|
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(entry.Key, entry.Value));
|
||||||
|
}
|
||||||
|
|
||||||
private void ForgetWait(string runId, string nodeId)
|
private void ForgetWait(string runId, string nodeId)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
@@ -347,7 +392,9 @@ public sealed class EffectsService
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
return ex is RudderApiException api
|
return ex is RudderApiException api
|
||||||
&& (api.Code == RudderErrorCodes.UnknownRun || api.Code == RudderErrorCodes.RunExpired);
|
&& (api.Code == RudderErrorCodes.UnknownRun
|
||||||
|
|| api.Code == RudderErrorCodes.RunExpired
|
||||||
|
|| api.Code == RudderErrorCodes.RunNotActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ effect. There is no local plan, no `IPlanStateStore`, and no restore.
|
|||||||
- This SDK does **not** fire a login scenario event (`loginEvent` /
|
- This SDK does **not** fire a login scenario event (`loginEvent` /
|
||||||
`player_login` is a web-SDK option only). Trigger login-gated scenarios
|
`player_login` is a web-SDK option only). Trigger login-gated scenarios
|
||||||
yourself with `TriggerAsync`.
|
yourself with `TriggerAsync`.
|
||||||
- An effect with an already-seen `(runId, nodeId)` is not re-emitted.
|
- An effect with an already-seen `(runId, nodeId)` is not re-emitted. The
|
||||||
|
seen set is pruned against every pending fetch: keys the server no longer
|
||||||
|
lists are dropped, so the server stays the source of truth.
|
||||||
- There is no local plan persistence. Pending work lives on the server.
|
- There is no local plan persistence. Pending work lives on the server.
|
||||||
|
|
||||||
## `ScenarioService` (`client.Scenario`)
|
## `ScenarioService` (`client.Scenario`)
|
||||||
@@ -210,7 +212,9 @@ public sealed class ScenarioFailedEffect
|
|||||||
```
|
```
|
||||||
|
|
||||||
`OnScenarioCompleted` fires when a callback / completed-counter response has
|
`OnScenarioCompleted` fires when a callback / completed-counter response has
|
||||||
no next effect (the run finished all its nodes).
|
no next effect (the run finished all its nodes). It also fires when a run
|
||||||
|
disappears from the pending list with no keys left for it — the server
|
||||||
|
advanced a wait, the run expired, or another device completed it.
|
||||||
|
|
||||||
## Reliability
|
## Reliability
|
||||||
|
|
||||||
@@ -219,7 +223,7 @@ no next effect (the run finished all its nodes).
|
|||||||
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
|
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
|
||||||
`onPremiumPurchase`. A non-null `effect` in the response is ingested as
|
`onPremiumPurchase`. A non-null `effect` in the response is ingested as
|
||||||
the next node; a null/missing effect completes the run.
|
the next node; a null/missing effect completes the run.
|
||||||
- `unknown_run`, `run_expired` (`RudderErrorCodes`), and HTTP 404
|
- `unknown_run`, `run_expired`, `run_not_active` (`RudderErrorCodes`), and HTTP 404
|
||||||
(`RudderNotFoundException`) on a callback or counter call **drop that
|
(`RudderNotFoundException`) on a callback or counter call **drop that
|
||||||
run** (forget its seen keys and wait deadlines) and fire
|
run** (forget its seen keys and wait deadlines) and fire
|
||||||
`OnScenarioFailed`.
|
`OnScenarioFailed`.
|
||||||
|
|||||||
Reference in New Issue
Block a user