/// <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);
/// <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); } }