/// <summary> Append one or more events to the stream. </summary> /// <remarks> /// Events are assigned consecutive sequence numbers. The FIRST of these /// numbers is returned. /// /// If no events are provided, return null. /// /// If this object's state no longer represents the remote stream (because other /// events have been written from elsewhere), this method will not write any /// events and will return null. The caller should call <see cref="FetchAsync"/> /// until it returns false to have the object catch up with remote state. /// </remarks> public async Task <uint?> WriteAsync(IReadOnlyList <TEvent> events, CancellationToken cancel = default(CancellationToken)) { if (events.Count == 0) { return(null); } if (Position < _minimumWritePosition) { return(null); } if (events.Any(e => e == null)) { throw new ArgumentException(@"No null events allowed", nameof(events)); } var sw = Stopwatch.StartNew(); var rawEvents = events.Select((e, i) => new RawEvent(_lastSequence + (uint)(i + 1), _serializer.Serialize(e))) .ToArray(); try { var result = await Storage.WriteAsync(Position, rawEvents, cancel); _minimumWritePosition = result.NextPosition; if (result.Success) { foreach (var e in rawEvents) { _cache.Enqueue(e); } Position = result.NextPosition; var first = _lastSequence + 1; _lastSequence += (uint)rawEvents.Length; _log?.Debug( $"Wrote {rawEvents.Length} events up to seq {_lastSequence} in {sw.Elapsed.TotalSeconds:F3}s."); return(first); } _log?.Debug($"Collision when writing {rawEvents.Length} events after {sw.Elapsed.TotalSeconds:F3}s."); return(null); } catch (Exception e) { _log?.Error($"When writing {rawEvents.Length} events after seq {_lastSequence}.", e); throw; } }
public ReifiedProjection(IProjection <TEvent, TState> projection, IProjectionCacheProvider cacheProvider = null, ILogAdapter log = null) { if (projection == null) { throw new ArgumentNullException(nameof(projection)); } // Cache the projection's full name. This shields us against projection authors // writing changing names. Name = projection.FullName; if (Name == null) { throw new ArgumentException("Projection must have a name", nameof(projection)); } var nameRegexp = new Regex("^[-a-zA-Z0-9_]{1,16}$"); if (!nameRegexp.IsMatch(Name)) { throw new ArgumentException("Projection name must match [-a-zA-Z0-9_]{1,16}", nameof(projection)); } _projection = projection; _cacheProvider = cacheProvider; _log = log; _log?.Debug("Using projection: " + Name); Reset(); }
/// <summary> /// Attempt to load this projection from the source, updating its /// <see cref="Current"/> and <see cref="Sequence"/>. /// </summary> /// <remarks> /// Object is unchanged if loading fails. /// /// Obviously, as this object does not support multi-threaded access, /// it should NOT be accessed in any way before the task has completed. /// </remarks> public async Task TryLoadAsync(CancellationToken cancel = default) { if (_cacheProvider == null) { _log?.Warning($"[{Name}] no read cache provider !"); return; } var sw = Stopwatch.StartNew(); IEnumerable <Task <CacheCandidate> > candidates; try { candidates = await _cacheProvider.OpenReadAsync(Name); } catch (Exception ex) { _log?.Warning($"[{Name}] error when opening cache.", ex); return; } foreach (var candidateTask in candidates) { CacheCandidate candidate; try { candidate = await candidateTask; } catch (Exception ex) { _log?.Warning($"[{Name}] error when opening cache.", ex); continue; } _log?.Info($"[{Name}] reading cache {candidate.Name}"); var stream = candidate.Contents; try { // Load the sequence number from the input uint seq; using (var br = new BinaryReader(stream, Encoding.UTF8, true)) seq = br.ReadUInt32(); _log?.Debug($"[{Name}] cache is at seq {seq}."); // Create a new stream to hide the write of the sequence numbers // (at the top and the bottom of the stream). var boundedStream = new BoundedStream(stream, stream.Length - 8); // Load the state, which advances the stream var state = await _projection.TryLoadAsync(boundedStream, cancel) .ConfigureAwait(false); if (state == null) { _log?.Warning($"[{Name}] projection could not parse cache {candidate.Name}"); continue; } // Sanity check: is the same sequence number found at the end ? uint endseq; using (var br = new BinaryReader(stream, Encoding.UTF8, true)) endseq = br.ReadUInt32(); if (endseq != seq) { _log?.Warning($"[{Name}] sanity-check seq is {endseq} in cache {candidate.Name}"); continue; } _log?.Info($"[{Name}] loaded {stream.Length} bytes in {sw.Elapsed:mm':'ss'.'fff} from cache {candidate.Name}"); Current = state; Sequence = seq; return; // Do NOT set _possiblyInconsistent to false here ! // Inconsistency can have external causes, e.g. event read // failure, that are not automagically solved by loading from cache. } catch (EndOfStreamException) { _log?.Warning($"[{Name}] incomplete cache {candidate.Name}"); // Incomplete streams are simply treated as missing } catch (Exception ex) { _log?.Warning($"[{Name}] could not parse cache {candidate.Name}", ex); // If a cache file cannot be parsed, try the next one } finally { stream.Dispose(); } } }
public void Debug(string message) { _log.Debug(Elapsed + " " + message); }
/// <inheritdoc/> public static void Debug(object caller, object message, params object[] replacements) => _log.Debug(caller, message, replacements);
/// <summary> /// Attempt to load this projection from the source, updating its /// <see cref="Current"/> and <see cref="Sequence"/>. /// </summary> /// <remarks> /// Object is unchanged if loading fails. /// /// Obviously, as this object does not support multi-threaded access, /// it should NOT be accessed in any way before the task has completed. /// </remarks> public async Task TryLoadAsync(CancellationToken cancel = default(CancellationToken)) { if (_cacheProvider == null) { _log?.Warning($"[{Name}] no read cache provider !"); return; } Stream source; var sw = Stopwatch.StartNew(); try { source = await _cacheProvider.OpenReadAsync(Name); } catch (Exception ex) { _log?.Warning($"[{Name}] error when opening cache.", ex); return; } if (source == null) { _log?.Info($"[{Name}] no cached data found."); return; } try { // Load the sequence number from the input uint seq; using (var br = new BinaryReader(source, Encoding.UTF8, true)) seq = br.ReadUInt32(); _log?.Debug($"[{Name}] cache is at seq {seq}."); // Load the state, which advances the stream var state = await _projection.TryLoadAsync(source, cancel).ConfigureAwait(false); if (state == null) { _log?.Warning($"[{Name}] projection could not parse cache."); return; } // Sanity check: is the same sequence number found at the end ? uint endseq; using (var br = new BinaryReader(source, Encoding.UTF8, true)) endseq = br.ReadUInt32(); if (endseq != seq) { _log?.Warning($"[{Name}] sanity-check seq is {endseq}."); return; } _log?.Info($"[{Name}] loaded from cache in {sw.Elapsed:mm':'ss'.'fff}."); Current = state; Sequence = seq; // Do NOT set _possiblyInconsistent to false here ! // Inconsistency can have external causes, e.g. event read // failure, that are not automagically solved by loading from cache. } catch (EndOfStreamException) { _log?.Warning($"[{Name}] cache is incomplete."); // Incomplete streams are simply treated as missing } catch (Exception ex) { _log?.Warning($"[{Name}] could not parse cache.", ex); throw; } }