/// <summary> /// Defers the message to the time specified by <paramref name="approximateDueTime"/> at which point in time the message will be /// returned to whoever calls <see cref="GetDueMessages"/> /// </summary> public async Task Defer(DateTimeOffset approximateDueTime, Dictionary <string, string> headers, byte[] body) { using (var connection = await _connectionProvider.GetConnectionAsync().ConfigureAwait(false)) { using (var command = connection.CreateCommand()) { command.CommandText = $@" INSERT INTO {_tableName.QualifiedName} ( `due_time`, `headers`, `body` ) VALUES ( @due_time, @headers, @body );"; var headersString = HeaderSerializer.SerializeToString(headers); command.Parameters.Add("due_time", MySqlDbType.DateTime).Value = approximateDueTime.UtcDateTime; command.Parameters.Add("headers", MySqlDbType.VarChar, MathUtil.GetNextPowerOfTwo(headersString.Length)).Value = headersString; command.Parameters.Add("body", MySqlDbType.VarBinary, MathUtil.GetNextPowerOfTwo(body.Length)).Value = body; await command.ExecuteNonQueryAsync().ConfigureAwait(false); } await connection.CompleteAsync().ConfigureAwait(false); } }
/// <summary> /// Saves a snapshot of the saga data along with the given metadata /// </summary> public async Task Save(ISagaData sagaData, Dictionary <string, string> sagaAuditMetadata) { using (var connection = await _connectionProvider.GetConnectionAsync()) { using (var command = connection.CreateCommand()) { command.CommandText = $@" INSERT INTO {_tableName.QualifiedName} ( `id`, `revision`, `data`, `metadata` ) VALUES ( @id, @revision, @data, @metadata )"; var dataString = DataSerializer.SerializeToString(sagaData); var metadataString = MetadataSerializer.SerializeToString(sagaAuditMetadata); command.Parameters.Add("id", MySqlDbType.Guid).Value = sagaData.Id; command.Parameters.Add("revision", MySqlDbType.Int32).Value = sagaData.Revision; command.Parameters.Add("data", MySqlDbType.VarChar, MathUtil.GetNextPowerOfTwo(dataString.Length)).Value = dataString; command.Parameters.Add("metadata", MySqlDbType.VarChar, MathUtil.GetNextPowerOfTwo(metadataString.Length)).Value = metadataString; Console.WriteLine($"OK WE'RE SAVING SAGA SNAPSHOT {sagaData.Id} rev. {sagaData.Revision} NOW"); await command.ExecuteNonQueryAsync().ConfigureAwait(false); } await connection.CompleteAsync().ConfigureAwait(false); } }
/// <summary> /// Acquire a lock for given key /// </summary> /// <param name="key">Locking key</param> /// <param name="cancellationToken">Cancellation token which will be cancelled if Rebus shuts down. Can be used if e.g. distributed locks have a timeout associated with them</param> /// <returns>True if the lock was acquired, false if not</returns> public async Task <bool> AcquireLockAsync(string key, CancellationToken cancellationToken) { using (var connection = await _connectionProvider.GetConnectionAsync().ConfigureAwait(false)) { // See if the lock exists and bail if someone already has it if (await IsLockAcquiredAsync(key, cancellationToken)) { return(false); } // Lock does not exist, so try to insert it. Because two threads could end up here at the same // time, if we get a duplicate entry exception, assume someone got there before us and fail out using (var command = connection.CreateCommand()) { command.CommandText = $@" INSERT INTO {_lockTableName.QualifiedName} ( lock_key, expiration ) VALUES ( @lock_key, @expiration )"; command.Parameters.Add("lock_key", MySqlDbType.VarChar, LockKeyColumnSize).Value = key; command.Parameters.Add("expiration", MySqlDbType.DateTime).Value = _rebusTime.Now.Add(_lockExpirationTimeout); try { await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); await connection.CompleteAsync().ConfigureAwait(false); } catch (MySqlException exception) when(exception.ErrorCode == MySqlErrorCode.DuplicateKeyEntry) { // Someone got there before us so bail and try again later return(false); } return(true); } } }
/// <summary> /// Gets all destination addresses for the given topic /// </summary> public async Task <string[]> GetSubscriberAddresses(string topic) { using (var connection = await _connectionProvider.GetConnectionAsync()) { using (var command = connection.CreateCommand()) { command.CommandText = $@" SELECT address FROM {_tableName.QualifiedName} WHERE topic = @topic"; command.Parameters.Add("topic", MySqlDbType.VarChar, _topicLength).Value = topic; var subscriberAddresses = new List <string>(); using (var reader = await command.ExecuteReaderAsync().ConfigureAwait(false)) { while (await reader.ReadAsync().ConfigureAwait(false)) { var address = (string)reader["address"]; subscriberAddresses.Add(address); } } return(subscriberAddresses.ToArray()); } } }
/// <summary> /// Gets the outbound message buffer for sending of messages /// </summary> /// <param name="context">Transaction context containing the message buffer</param> ConcurrentQueue <AddressedTransportMessage> GetOutboundMessageBuffer(ITransactionContext context) { return(context.GetOrAdd(OutboundMessageBufferKey, () => { var outgoingMessages = new ConcurrentQueue <AddressedTransportMessage>(); async Task SendOutgoingMessages(ITransactionContext _) { using (var connection = await _connectionProvider.GetConnectionAsync()) { while (outgoingMessages.IsEmpty == false) { if (outgoingMessages.TryDequeue(out var addressed) == false) { break; } await InnerSend(addressed.DestinationAddress, addressed.Message, connection); }
/// <summary> /// Saves the data from the given source stream under the given ID /// </summary> public async Task Save(string id, Stream source, Dictionary <string, string> metadata = null) { var metadataToWrite = new Dictionary <string, string>(metadata ?? new Dictionary <string, string>()) { [MetadataKeys.SaveTime] = _rebusTime.Now.ToString("O") }; try { using (var connection = await _connectionProvider.GetConnectionAsync().ConfigureAwait(false)) { using (var command = connection.CreateCommand()) { var metadataBytes = TextEncoding.GetBytes(_dictionarySerializer.SerializeToString(metadataToWrite)); using (var memoryStream = new MemoryStream()) { await source.CopyToAsync(memoryStream); var sourceBytes = memoryStream.ToArray(); command.CommandTimeout = _commandTimeout; command.CommandText = $@" INSERT INTO {_tableName.QualifiedName} ( `id`, `meta`, `data`, `creation_time`, `last_read_time`) VALUES ( @id, @meta, @data, @now, null )"; command.Parameters.Add("id", MySqlDbType.VarChar, 200).Value = id; command.Parameters.Add("meta", MySqlDbType.VarBinary, MathUtil.GetNextPowerOfTwo(metadataBytes.Length)).Value = metadataBytes; command.Parameters.Add("data", MySqlDbType.VarBinary, MathUtil.GetNextPowerOfTwo(sourceBytes.Length)).Value = sourceBytes; command.Parameters.Add("now", MySqlDbType.DateTime).Value = _rebusTime.Now.DateTime; await command.ExecuteNonQueryAsync().ConfigureAwait(false); } } await connection.CompleteAsync().ConfigureAwait(false); } } catch (Exception exception) { throw new RebusApplicationException(exception, $"Could not save data with ID {id}"); } }
/// <summary> /// Handle retrieving a message from the queue, decoding it, and performing any transaction maintenance. /// </summary> /// <param name="context">Transaction context the receive is operating on</param> /// <param name="cancellationToken">Token to abort processing</param> /// <returns>A <seealso cref="TransportMessage"/> or <c>null</c> if no message can be dequeued</returns> protected virtual async Task <TransportMessage> ReceiveInternal(ITransactionContext context, CancellationToken cancellationToken) { TransportMessage transportMessage; using (var connection = await _connectionProvider.GetConnectionAsync().ConfigureAwait(false)) { using (var command = connection.CreateCommand()) { var tableName = _receiveTableName.QualifiedName; command.CommandText = $@" SELECT id INTO @id FROM {tableName} WHERE visible < now(6) AND expiration > now(6) AND processing = 0 ORDER BY priority DESC, visible ASC, id ASC LIMIT 1 FOR UPDATE; SELECT id, headers, body FROM {tableName} WHERE id = @id LIMIT 1; UPDATE {tableName} SET processing = 1 WHERE id = @id; SET @id = null"; try { using (var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false)) { transportMessage = await ExtractTransportMessageFromReader(reader, cancellationToken).ConfigureAwait(false); if (transportMessage == null) { return(null); } var messageId = (long)reader["id"]; ApplyTransactionSemantics(context, messageId); } } catch (MySqlException exception) when(exception.ErrorCode == MySqlErrorCode.LockDeadlock) { // If we get a transaction deadlock here, simply return null and assume there is nothing to process return(null); } catch (Exception exception) when(cancellationToken.IsCancellationRequested) { // ADO.NET does not throw the right exception when the task gets cancelled - therefore we need to do this: throw new TaskCanceledException("Receive operation was cancelled", exception); } } await connection.CompleteAsync().ConfigureAwait(false); } return(transportMessage); }
/// <summary> /// Queries the saga index for an instance with the given <paramref name="sagaDataType"/> with a /// a property named <paramref name="propertyName"/> and the value <paramref name="propertyValue"/> /// </summary> public async Task <ISagaData> Find(Type sagaDataType, string propertyName, object propertyValue) { if (sagaDataType == null) { throw new ArgumentNullException(nameof(sagaDataType)); } if (propertyName == null) { throw new ArgumentNullException(nameof(propertyName)); } if (propertyValue == null) { throw new ArgumentNullException(nameof(propertyValue)); } using (var connection = await _connectionProvider.GetConnectionAsync().ConfigureAwait(false)) { using (var command = connection.CreateCommand()) { if (propertyName.Equals(IdPropertyName, StringComparison.OrdinalIgnoreCase)) { command.CommandText = $@" SELECT data FROM {_dataTableName.QualifiedName} WHERE id = @value LIMIT 1"; } else { command.CommandText = $@" SELECT s.data AS data FROM {_dataTableName.QualifiedName} s JOIN {_indexTableName.QualifiedName} i ON (s.id = i.saga_id) WHERE i.`saga_type` = @saga_type AND i.`key` = @key AND i.`value` = @value LIMIT 1"; var sagaTypeName = GetSagaTypeName(sagaDataType); command.Parameters.Add("key", MySqlDbType.VarChar, propertyName.Length).Value = propertyName; command.Parameters.Add("saga_type", MySqlDbType.VarChar, sagaTypeName.Length).Value = sagaTypeName; } var correlationPropertyValue = GetCorrelationPropertyValue(propertyValue); command.Parameters.Add("value", MySqlDbType.VarChar, MathUtil.GetNextPowerOfTwo(correlationPropertyValue.Length)).Value = correlationPropertyValue; using (var reader = await command.ExecuteReaderAsync().ConfigureAwait(false)) { if (!await reader.ReadAsync().ConfigureAwait(false)) { return(null); } var value = GetData(reader); try { var sagaData = _sagaSerializer.DeserializeFromString(sagaDataType, value); return(sagaData); } catch (Exception exception) { throw new RebusApplicationException(exception, $"An error occurred while attempting to deserialize '{value}' into a {sagaDataType}"); } } } } }
public async Task CommitAsync(string package, IPackagePatch patch) { using (var connection = await _connectionProvider.GetConnectionAsync().ConfigureAwait(false)) using (var transaction = connection.BeginTransaction()) { var tasks = new List <Task>(); // Publish versions if (patch.PublishedVersions?.Count > 0) { var packages = new List <PackageVersionEntity>(); var tarballs = new List <TarballEntity>(); foreach (var version in patch.PublishedVersions) { packages.Add(ToEntity(version)); tarballs.Add(ToEntity(version.Tarball)); } tasks.Add(connection.ExecuteAsync(CreatePackageVersionQuery, packages, transaction)); tasks.Add(connection.ExecuteAsync(CreateTarballQuery, tarballs, transaction)); } // Update versions if (patch.UpdatedVersions?.Count > 0) { var versions = patch.UpdatedVersions.Select(ToEntity); tasks.Add(connection.ExecuteAsync(UpdatePackageVersionQuery, versions, transaction)); } // Delete versions if (patch.DeletedVersions?.Count > 0) { var versions = patch.DeletedVersions.Select(id => new PackageVersionEntity() { Package = id.Name, Version = id.Version, Published = false, Manifest = null // This is COALESCED in the UPDATE so it won't actually clobber the manifest }); tasks.Add(connection.ExecuteAsync(UpdatePackageVersionQuery, versions, transaction)); } // Update/create dist tags if (patch.UpdatedDistTags?.Count > 0) { var tags = patch.UpdatedDistTags.Select(kvp => new DistTagEntity() { Package = package, Tag = kvp.Key, Version = kvp.Value }); tasks.Add(connection.ExecuteAsync(CreateDistTagQuery, tags, transaction)); } // Delete dist tags if (patch.DeletedDistTags?.Count > 0) { var param = new { Package = package, Tags = patch.DeletedDistTags }; tasks.Add(connection.ExecuteAsync(DeleteDistTagsQuery, param, transaction)); } try { await Task.WhenAll(tasks).ConfigureAwait(false); transaction.Commit(); } catch (SqliteException ex) { if (ex.SqliteErrorCode != ErrorSqliteConstraint || ex.SqliteExtendedErrorCode != ErrorSqliteConstraintUnique) { throw; } var identifier = patch.PublishedVersions?.Count == 1 ? patch.PublishedVersions[0].Id : null; throw new DuplicatePackageVersionException(identifier, ex); } } }