/// <summary>
 /// Instructs the server to start the Logical Streaming Replication Protocol (pgoutput logical decoding plugin),
 /// starting at WAL location <paramref name="walLocation"/> or at the slot's consistent point if <paramref name="walLocation"/>
 /// isn't specified.
 /// The server can reply with an error, for example if the requested section of the WAL has already been recycled.
 /// </summary>
 /// <param name="connection">The <see cref="LogicalReplicationConnection"/> to use for starting replication</param>
 /// <param name="slot">The replication slot that will be updated as replication progresses so that the server
 /// knows which WAL segments are still needed by the standby.
 /// </param>
 /// <param name="options">The collection of options passed to the slot's logical decoding plugin.</param>
 /// <param name="cancellationToken">The token to monitor for stopping the replication.</param>
 /// <param name="walLocation">The WAL location to begin streaming at.</param>
 /// <returns>A <see cref="Task{T}"/> representing an <see cref="IAsyncEnumerable{T}"/> that
 /// can be used to stream WAL entries in form of <see cref="PgOutputReplicationMessage"/> instances.</returns>
 public static IAsyncEnumerable <PgOutputReplicationMessage> StartReplication(
     this LogicalReplicationConnection connection,
     PgOutputReplicationSlot slot,
     PgOutputReplicationOptions options,
     CancellationToken cancellationToken,
     NpgsqlLogSequenceNumber?walLocation = null)
 => new PgOutputAsyncEnumerable(connection, slot, options, cancellationToken, walLocation);
Ejemplo n.º 2
0
        /// <summary>
        /// Subscribes to the Postgres replication.
        /// </summary>
        /// <returns></returns>
        /// <remarks>
        /// Source: https://www.postgresql.org/docs/10/protocol-logical-replication.html
        /// The logical replication protocol sends individual transactions one by one. This means that all messages between a pair of
        /// Begin and Commit messages belong to the same transaction. Every sent transaction contains zero or more DML messages
        /// (Insert, Update, Delete). In case of a cascaded setup it can also contain Origin messages. The origin message indicates
        /// that the transaction originated on different replication node. Since a replication node in the scope of logical replication
        /// protocol can be pretty much anything, the only identifier is the origin name.It's downstream's responsibility to handle
        /// this as needed (if needed).The Origin message is always sent before any DML messages in the transaction.
        ///
        /// Every DML message contains an arbitrary relation ID, which can be mapped to an ID in the Relation messages.
        /// The Relation messages describe the schema of the given relation. The Relation message is sent for a given relation either
        /// because it is the first time we send a DML message for given relation in the current session or because the relation
        /// definition has changed since the last Relation message was sent for it.The protocol assumes that the client is capable of
        /// caching the metadata for as many relations as needed.
        /// </remarks>
        private async Task SubscribeAsync()
        {
            Transaction transactionEvent = null;

            var publication = new PgOutputReplicationOptions(_config.PublicationName, 1);
            var slot        = new PgOutputReplicationSlot(_config.ReplicationSlotName);

            var replication = _connection.StartReplication(slot, publication, _cancelTokenSource.Token);

            Logger.LogInformation($"Start replication at slot '{_config.ReplicationSlotName}'");

            await foreach (var message in replication)
            {
                var messageType = message.GetType();
                if (messageType == typeof(BeginMessage))
                {
                    transactionEvent = new Transaction();
                }
                else if (messageType == typeof(CommitMessage))
                {
                    EmitEvent(transactionEvent);
                }
                else if (messageType == typeof(RelationMessage))
                {
                    var relationMsg = message as RelationMessage;
                    if (!_relations.ContainsKey(relationMsg.RelationId))
                    {
                        _relations.Add(relationMsg.RelationId, new Relation
                        {
                            Id          = relationMsg.RelationId,
                            Name        = relationMsg.RelationName,
                            ColumnNames = relationMsg.Columns.ToArray().Select(x => x.ColumnName).ToArray()
                        });
                    }
                }
                else if (messageType == typeof(InsertMessage))
                {
                    var insertMsg = message as InsertMessage;
                    transactionEvent.DataEvents.Add(new InsertEvent
                    {
                        Relation     = _relations.ContainsKey(insertMsg.Relation.RelationId) ? _relations[insertMsg.Relation.RelationId] : null,
                        ColumnValues = await ToStringArrayAsync(insertMsg.NewRow)
                    });
                }
                else if (messageType == typeof(DefaultUpdateMessage))
                {
                    var updateMsg = message as DefaultUpdateMessage;
                    transactionEvent.DataEvents.Add(new UpdateEvent
                    {
                        Relation     = _relations.ContainsKey(updateMsg.Relation.RelationId) ? _relations[updateMsg.Relation.RelationId] : null,
                        ColumnValues = await ToStringArrayAsync(updateMsg.NewRow)
                    });
                }
                else if (messageType == typeof(FullUpdateMessage))
                {
                    var updateMsg = message as FullUpdateMessage;
                    transactionEvent.DataEvents.Add(new UpdateEvent
                    {
                        Relation        = _relations.ContainsKey(updateMsg.Relation.RelationId) ? _relations[updateMsg.Relation.RelationId] : null,
                        ColumnValues    = await ToStringArrayAsync(updateMsg.NewRow),
                        OldColumnValues = await ToStringArrayAsync(updateMsg.OldRow)
                    });
                }
                else if (messageType == typeof(KeyDeleteMessage))
                {
                    var deleteMsg = message as KeyDeleteMessage;
                    transactionEvent.DataEvents.Add(new DeleteEvent
                    {
                        Relation = _relations.ContainsKey(deleteMsg.Relation.RelationId) ? _relations[deleteMsg.Relation.RelationId] : null,
                        Keys     = await ToStringArrayAsync(deleteMsg.Key)
                    });
                }
                else if (messageType == typeof(FullDeleteMessage))
                {
                    var deleteMsg = message as FullDeleteMessage;
                    transactionEvent.DataEvents.Add(new DeleteEvent
                    {
                        Relation        = _relations.ContainsKey(deleteMsg.Relation.RelationId) ? _relations[deleteMsg.Relation.RelationId] : null,
                        OldColumnValues = await ToStringArrayAsync(deleteMsg.OldRow)
                    });
                }

                // Inform the server which WAL files can removed or recycled.
                _connection.SetReplicationStatus(message.WalEnd);
            }
        }