internal async Task <int> TryScavenge( StreamIdInfo streamIdInfo, CancellationToken cancellationToken) { if (streamIdInfo.PostgresqlStreamId == PostgresqlStreamId.Deleted) { return(-1); } try { using (var connection = await OpenConnection(cancellationToken)) using (var transaction = connection.BeginTransaction()) { var deletedMessageIds = new List <Guid>(); using (var command = BuildFunctionCommand( _schema.Scavenge, transaction, Parameters.StreamId(streamIdInfo.PostgresqlStreamId))) using (var reader = await command.ExecuteReaderAsync(cancellationToken).NotOnCapturedContext()) { while (await reader.ReadAsync(cancellationToken).NotOnCapturedContext()) { deletedMessageIds.Add(reader.GetGuid(0)); } } if (Logger.IsInfoEnabled()) { Logger.Info($"Found {deletedMessageIds.Count} message(s) for stream {streamIdInfo.PostgresqlStreamId} to scavenge."); } if (deletedMessageIds.Count > 0) { if (Logger.IsDebugEnabled()) { Logger.Debug($"Scavenging the following messages on stream {streamIdInfo.PostgresqlStreamId}: {string.Join(", ", deletedMessageIds)}"); } await DeleteEventsInternal( streamIdInfo, deletedMessageIds.ToArray(), transaction, cancellationToken).NotOnCapturedContext(); } await transaction.CommitAsync(cancellationToken).NotOnCapturedContext(); return(deletedMessageIds.Count); } } catch (Exception ex) { if (Logger.IsWarnEnabled()) { Logger.WarnException( $"Scavenge attempt failed on stream {streamIdInfo.PostgresqlStreamId.IdOriginal}. Another attempt will be made when this stream is written to.", ex); } } return(-1); }
internal async Task <int> TryScavenge( StreamIdInfo streamId, CancellationToken cancellationToken) { if (streamId.MySqlStreamId == MySqlStreamId.Deleted) { return(-1); } try { using (var connection = await OpenConnection(cancellationToken)) using (var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false)) { var deletedMessageIds = new List <Guid>(); using (var command = BuildStoredProcedureCall( _schema.Scavenge, transaction, Parameters.StreamId(streamId.MySqlStreamId))) using (var reader = await command .ExecuteReaderAsync(cancellationToken) .ConfigureAwait(false)) { while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { deletedMessageIds.Add(reader.GetGuid(0)); } } Logger.Info( "Found {deletedMessageIdCount} message(s) for stream {streamId} to scavenge.", deletedMessageIds.Count, streamId.MySqlStreamId.IdOriginal); if (deletedMessageIds.Count > 0) { Logger.Debug( "Scavenging the following messages on stream {streamId}: {deletedMessageIds}", streamId.MySqlStreamId.IdOriginal, deletedMessageIds); } foreach (var deletedMessageId in deletedMessageIds) { await DeleteEventInternal(streamId, deletedMessageId, transaction, cancellationToken); } await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); return(deletedMessageIds.Count); } } catch (Exception ex) { Logger.WarnException( "Scavenge attempt failed on stream {streamId}. Another attempt will be made when this stream is written to.", ex, streamId.MySqlStreamId.IdOriginal); } return(-1); }
protected override async Task <AppendResult> AppendToStreamInternal( string streamId, int expectedVersion, NewStreamMessage[] messages, CancellationToken cancellationToken) { int maxRetries = 2; //TODO too much? too little? configurable? Exception exception; int retryCount = 0; do { try { AppendResult result; var streamIdInfo = new StreamIdInfo(streamId); using (var connection = await OpenConnection(cancellationToken)) using (var transaction = connection.BeginTransaction()) using (var command = BuildFunctionCommand( _schema.AppendToStream, transaction, Parameters.StreamId(streamIdInfo.PostgresqlStreamId), Parameters.StreamIdOriginal(streamIdInfo.PostgresqlStreamId), Parameters.MetadataStreamId(streamIdInfo.MetadataPosgresqlStreamId), Parameters.ExpectedVersion(expectedVersion), Parameters.CreatedUtc(_settings.GetUtcNow?.Invoke()), Parameters.NewStreamMessages(messages))) { try { using (var reader = await command .ExecuteReaderAsync(cancellationToken) .NotOnCapturedContext()) { await reader.ReadAsync(cancellationToken).NotOnCapturedContext(); result = new AppendResult(reader.GetInt32(0), reader.GetInt64(1)); } await transaction.CommitAsync(cancellationToken).NotOnCapturedContext(); } catch (PostgresException ex) when(ex.IsWrongExpectedVersion()) { await transaction.RollbackAsync(cancellationToken).NotOnCapturedContext(); throw new WrongExpectedVersionException( ErrorMessages.AppendFailedWrongExpectedVersion( streamIdInfo.PostgresqlStreamId.IdOriginal, expectedVersion), ex); } } if (_settings.ScavengeAsynchronously) { #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed Task.Run(() => TryScavenge(streamIdInfo, cancellationToken), cancellationToken); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } else { await TryScavenge(streamIdInfo, cancellationToken).NotOnCapturedContext(); } return(result); } catch (PostgresException ex) when(ex.IsDeadlock()) { exception = ex; retryCount++; } } while(retryCount < maxRetries); ExceptionDispatchInfo.Capture(exception).Throw(); return(default); // never actually run
protected override async Task <SetStreamMetadataResult> SetStreamMetadataInternal( string streamId, int expectedStreamMetadataVersion, int?maxAge, int?maxCount, string metadataJson, CancellationToken cancellationToken) { var metadata = new MetadataMessage { StreamId = streamId, MaxAge = maxAge, MaxCount = maxCount, MetaJson = metadataJson }; var metadataMessageJsonData = SimpleJson.SerializeObject(metadata); var streamIdInfo = new StreamIdInfo(streamId); var currentVersion = Parameters.CurrentVersion(); try { using (var connection = await OpenConnection(cancellationToken)) using (var transaction = await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false)) using (var command = BuildStoredProcedureCall( _schema.SetStreamMetadata, transaction, Parameters.StreamId(streamIdInfo.MySqlStreamId), Parameters.MetadataStreamId(streamIdInfo.MetadataMySqlStreamId), Parameters.MetadataStreamIdOriginal(streamIdInfo.MetadataMySqlStreamId), Parameters.OptionalMaxAge(metadata.MaxAge), Parameters.OptionalMaxCount(metadata.MaxCount), Parameters.ExpectedVersion(expectedStreamMetadataVersion), Parameters.CreatedUtc(_settings.GetUtcNow?.Invoke()), Parameters.MetadataMessageMessageId( streamIdInfo.MetadataMySqlStreamId, expectedStreamMetadataVersion, metadataMessageJsonData), Parameters.MetadataMessageType(), Parameters.MetadataMessageJsonData(metadataMessageJsonData), currentVersion, Parameters.CurrentPosition())) { await command.ExecuteNonQueryAsync(cancellationToken).NotOnCapturedContext(); await transaction.CommitAsync(cancellationToken).NotOnCapturedContext(); } await TryScavenge(streamIdInfo, cancellationToken); return(new SetStreamMetadataResult((int)currentVersion.Value)); } catch (MySqlException ex) when(ex.IsWrongExpectedVersion()) { throw new WrongExpectedVersionException( ErrorMessages.AppendFailedWrongExpectedVersion( streamIdInfo.MetadataMySqlStreamId.IdOriginal, expectedStreamMetadataVersion), streamIdInfo.MetadataMySqlStreamId.IdOriginal, ExpectedVersion.Any, ex); } }
protected override async Task <ReadAllPage> ReadAllForwardsInternal( long fromPositionExlusive, int maxCount, bool prefetch, ReadNextAllPage readNext, CancellationToken cancellationToken) { maxCount = maxCount == int.MaxValue ? maxCount - 1 : maxCount; long ordinal = fromPositionExlusive; using (var connection = _createConnection()) { await connection.OpenAsync(cancellationToken).NotOnCapturedContext(); var commandText = prefetch ? _scripts.ReadAllForwardWithData : _scripts.ReadAllForward; using (var command = new SqlCommand(commandText, connection)) { command.Parameters.AddWithValue("ordinal", ordinal); command.Parameters.AddWithValue("count", maxCount + 1); //Read extra row to see if at end or not var reader = await command .ExecuteReaderAsync(cancellationToken) .NotOnCapturedContext(); List <StreamMessage> messages = new List <StreamMessage>(); if (!reader.HasRows) { return(new ReadAllPage( fromPositionExlusive, fromPositionExlusive, true, ReadDirection.Forward, readNext, messages.ToArray())); } while (await reader.ReadAsync(cancellationToken).NotOnCapturedContext()) { if (messages.Count == maxCount) { messages.Add(default(StreamMessage)); } else { var streamId = reader.GetString(0); var streamVersion = reader.GetInt32(1); ordinal = reader.GetInt64(2); var eventId = reader.GetGuid(3); var created = reader.GetDateTime(4); var type = reader.GetString(5); var jsonMetadata = reader.GetString(6); Func <CancellationToken, Task <string> > getJsonData; if (prefetch) { var jsonData = reader.GetString(7); getJsonData = _ => Task.FromResult(jsonData); } else { var streamIdInfo = new StreamIdInfo(streamId); getJsonData = ct => GetJsonData(streamIdInfo.SqlStreamId.Id, streamVersion, ct); } var message = new StreamMessage(streamId, eventId, streamVersion, ordinal, created, type, jsonMetadata, getJsonData); messages.Add(message); } } bool isEnd = true; if (messages.Count == maxCount + 1) // An extra row was read, we're not at the end { isEnd = false; messages.RemoveAt(maxCount); } var nextPosition = messages[messages.Count - 1].Position + 1; return(new ReadAllPage( fromPositionExlusive, nextPosition, isEnd, ReadDirection.Forward, readNext, messages.ToArray())); } } }
protected override async Task <SetStreamMetadataResult> SetStreamMetadataInternal( string streamId, int expectedStreamMetadataVersion, int?maxAge, int?maxCount, string metadataJson, CancellationToken cancellationToken) { MsSqlAppendResult result; using (var connection = _createConnection()) { var streamIdInfo = new StreamIdInfo(streamId); await connection.OpenAsync(cancellationToken).NotOnCapturedContext(); using (var transaction = connection.BeginTransaction()) { var metadataMessage = new MetadataMessage { StreamId = streamId, MaxAge = maxAge, MaxCount = maxCount, MetaJson = metadataJson }; var json = SimpleJson.SerializeObject(metadataMessage); var messageId = MetadataMessageIdGenerator.Create( streamId, expectedStreamMetadataVersion, json); var newStreamMessage = new NewStreamMessage(messageId, "$stream-metadata", json); result = await AppendToStreamInternal( connection, transaction, streamIdInfo.MetadataSqlStreamId, expectedStreamMetadataVersion, new[] { newStreamMessage }, cancellationToken); using (var command = new SqlCommand(_scripts.SetStreamMetadata, connection, transaction)) { command.Parameters.Add(new SqlParameter("streamId", SqlDbType.Char, 42) { Value = streamIdInfo.SqlStreamId.Id }); command.Parameters.AddWithValue("streamIdOriginal", streamIdInfo.SqlStreamId.IdOriginal); command.Parameters.Add("maxAge", SqlDbType.Int); command.Parameters["maxAge"].Value = maxAge ?? -1; command.Parameters.Add("maxCount", SqlDbType.Int); command.Parameters["maxCount"].Value = maxCount ?? -1; await command.ExecuteNonQueryAsync(cancellationToken); } transaction.Commit(); } } await CheckStreamMaxCount(streamId, maxCount, cancellationToken); return(new SetStreamMetadataResult(result.CurrentVersion)); }
protected override async Task <AppendResult> AppendToStreamInternal( string streamId, int expectedVersion, NewStreamMessage[] messages, CancellationToken cancellationToken) { var streamIdInfo = new StreamIdInfo(streamId); if (_settings.AppendDeadlockRetryAttempts == 0) { try { return(messages.Length == 0 ? await CreateEmptyStream(streamIdInfo, expectedVersion, cancellationToken) : await AppendMessagesToStream(streamIdInfo, expectedVersion, messages, cancellationToken)); } catch (MySqlException ex) when(ex.IsWrongExpectedVersion()) { throw new WrongExpectedVersionException( ErrorMessages.AppendFailedWrongExpectedVersion( streamIdInfo.MySqlStreamId.IdOriginal, expectedVersion), streamIdInfo.MySqlStreamId.IdOriginal, expectedVersion, ex); } } var retryableExceptions = new List <Exception>(); while (retryableExceptions.Count <= _settings.AppendDeadlockRetryAttempts) { try { return(messages.Length == 0 ? await CreateEmptyStream(streamIdInfo, expectedVersion, cancellationToken) : await AppendMessagesToStream(streamIdInfo, expectedVersion, messages, cancellationToken)); } catch (MySqlException ex) when(ex.IsDeadlock()) { retryableExceptions.Add(ex); } catch (MySqlException ex) when(ex.IsWrongExpectedVersion()) { throw new WrongExpectedVersionException( ErrorMessages.AppendFailedWrongExpectedVersion( streamIdInfo.MySqlStreamId.IdOriginal, expectedVersion), streamIdInfo.MySqlStreamId.IdOriginal, expectedVersion, ex); } } throw new WrongExpectedVersionException( MySqlErrorMessages.AppendFailedDeadlock( streamIdInfo.MySqlStreamId.IdOriginal, expectedVersion, _settings.AppendDeadlockRetryAttempts), streamIdInfo.MySqlStreamId.IdOriginal, expectedVersion, new AggregateException(retryableExceptions)); }
protected override async Task <ReadAllPage> ReadAllForwardsInternal( long fromPosition, int maxCount, bool prefetch, ReadNextAllPage readNext, CancellationToken cancellationToken) { maxCount = maxCount == int.MaxValue ? maxCount - 1 : maxCount; long position = fromPosition; using (var connection = _createConnection()) { await connection.OpenAsync(cancellationToken).ConfigureAwait(false); var commandText = prefetch ? _scripts.ReadAllForwardWithData : _scripts.ReadAllForward; using (var command = new SqlCommand(commandText, connection)) { command.CommandTimeout = _commandTimeout; command.Parameters.AddWithValue("position", position); command.Parameters.AddWithValue("count", maxCount + 1); //Read extra row to see if at end or not var reader = await command .ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken) .ConfigureAwait(false); if (!reader.HasRows) { return(new ReadAllPage( fromPosition, fromPosition, true, ReadDirection.Forward, readNext, Array.Empty <StreamMessage>())); } var messages = new List <(StreamMessage, int?)>(); while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { var ordinal = 0; var streamId = reader.GetString(ordinal++); var maxAge = reader.GetNullableInt32(ordinal++); var streamVersion = reader.GetInt32(ordinal++); position = reader.GetInt64(ordinal++); var eventId = reader.GetGuid(ordinal++); var created = reader.GetDateTime(ordinal++); var type = reader.GetString(ordinal++); var jsonMetadata = reader.GetString(ordinal++); Func <CancellationToken, Task <string> > getJsonData; if (prefetch) { var jsonData = await reader.GetTextReader(ordinal).ReadToEndAsync(); getJsonData = _ => Task.FromResult(jsonData); } else { var streamIdInfo = new StreamIdInfo(streamId); getJsonData = ct => GetJsonData(streamIdInfo.SqlStreamId.Id, streamVersion, ct); } var message = new StreamMessage(streamId, eventId, streamVersion, position, created, type, jsonMetadata, getJsonData); messages.Add((message, maxAge)); } bool isEnd = true; if (messages.Count == maxCount + 1) // An extra row was read, we're not at the end { isEnd = false; messages.RemoveAt(maxCount); } var filteredMessages = FilterExpired(messages); var nextPosition = filteredMessages[filteredMessages.Count - 1].Position + 1; return(new ReadAllPage( fromPosition, nextPosition, isEnd, ReadDirection.Forward, readNext, filteredMessages.ToArray())); } } }