/// <summary> /// Starts reclaiming transaction log entries that are no longer reachable. Typically called in a fire-and-forget fashion. /// </summary> /// <returns>Task to indicate when garbage collection is finished.</returns> public async Task ReclaimAsync() { _parent.AssertInvariants(); if (_parent._heldCount > _parent._activeCount) { using (await _parent._lock.EnterAsync().ConfigureAwait(false)) { if (_parent._heldCount > _parent._activeCount) { Tracing.Transaction_Log_Garbage_Collection_Start(null, _parent._engineId, _parent._latest, _parent._activeCount, _parent._heldCount); var difference = _parent._heldCount - _parent._activeCount; using (var tx = _parent._keyValueStore.CreateTransaction()) { foreach (var table in _parent._versionedLogs.Take((int)difference)) { table.Scope(tx).Clear(); } var metadata = _parent._metadataTable.Enter(tx); metadata.Update(HeldCountKey, _parent._activeCount.ToString(CultureInfo.InvariantCulture)); await tx.CommitAsync().ConfigureAwait(false); Tracing.Transaction_Log_Garbage_Collection(null, _parent._engineId, difference, _parent._latest, _parent._activeCount); for (var i = 0; i < difference; i++) { _parent._versionedLogs.Dequeue(); } _parent._heldCount = _parent._activeCount; } Tracing.Transaction_Log_Garbage_Collection_End(null, _parent._engineId, _parent._latest, _parent._activeCount, _parent._heldCount); } } } _parent.AssertInvariants(); }
/// <summary> /// Triggers truncation of the transaction log (everything up to and including the current snapshot that has been /// superseded by the state persisted by the successful checkpoint). /// </summary> /// <param name="transaction">The transaction to use to update the underlying tables.</param> /// <returns> /// Task completing when metadata has been updated to indicate the intent to truncate; the returned object is then used /// to trigger a background GC using <see cref="ReclaimResource.ReclaimAsync"/>. /// </returns> /// <remarks> /// If a failure happens after a successful checkpoint but before calling (and completing) the lose reference operation, /// subsequent recovery will cause coalescing of the not-yet-discarded snapshotted-but-checkpointed log entries with any /// newer entries that were added. We're hardended against these operations that will cause "create existing" and "delete /// non-existing" resources, but it's not ideal. /// /// When/if we supersede the IStateWriter approach to checkpoints and fully commit to using the key/value store passed to /// the engine's constructor, we can have a transaction that spans both updates, and not have to worry about this edge case. /// /// The reason for having the stores in two places is historical when early versions of Reaqtor managed the transaction log /// externally to an engine using specialized transaction log facilities (akin to CLFS) that were not integrated with the /// key/value store used (i.e. Service Fabric KVS). Over time, we moved away from this model, but we had to keep the /// IStateWriter and IStateReader that's used in other environments as well. /// /// It's worth considering an alternative engine implementation (alongside the current one for starters) that's "active" /// and doesn't have to be told when to checkpoint either. It simply uses the key/value store passed to its constructor, /// supports `RecoverAsync()` to recover from the given store (or has a static factory to do so, rather than a separate /// construction step), and performs checkpointing when it needs to, akin to a GC deciding it's time to perform a GC. It /// can do this by measuring the amount of dirty state and maybe have a configure maximum delay in between checkpoints to /// ensure recovery time is bounded (and replay of events is limited). It can furthermore have probes on ingress pieces /// (reachable through some IIngressProbe interface implemented by operators, so it can hunt for these using visitors over /// the subscriptions and subjects) to compute metrics and be self-tuning. There can still be a `CheckpointAsync` which /// is similar to `GC.Collect()` where an external party can trigger a checkpoint, e.g. right before unloading the engine /// or when a known burst of events has passed through, so it's worth evacuating state to disk. /// </remarks> public async Task <ReclaimResource> LoseReferenceAsync(IKeyValueStoreTransaction transaction = null) { _parent.AssertInvariants(); var createdNewTransaction = false; if (transaction == null) { createdNewTransaction = true; transaction = _parent._keyValueStore.CreateTransaction(); } // Very important to make the active count = 1 at the very least. Garbage collection is secondary (but important too). using (await _parent._lock.EnterAsync().ConfigureAwait(false)) { var tx = _parent._metadataTable.Enter(transaction); try { tx.Update(ActiveCountKey, "1"); await transaction.CommitAsync().ConfigureAwait(false); } finally { if (createdNewTransaction) { transaction.Dispose(); } } _parent._activeCount = 1; } Tracing.Transaction_Log_Lost_Reference(null, _parent._engineId, _parent._latest, _parent._activeCount, _parent._heldCount); _parent.AssertInvariants(); return(new ReclaimResource(_parent)); }