SQL Server: fill a gap in GlobalPosition with a tombstone (GapHandlingTimeoutMs)
Maintainer thường phản hồi trong vòng 7 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 48/100
Hướng nghiên cứu
Start by reading SqlSubscriptionBase.HandleGapTimeout, SqlServerSubscriptionBase, and the PostgreSQL implementation that calls 9_TryInsertTombstone.sql; then inspect the SQL Server schema procedures and subscription tests. Add SQL Server tombstone insertion using GapHandlingTimeoutMs and ensure tombstones are skipped. Done means a rolled-back identity gap is filled without skipping a slow committed append, and the subscription resumes.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
On SQL Server, a rolled back append leaves a gap in GlobalPosition, and every all-stream subscription stops at that gap. The subscription waits until the event after the gap is older than GapAgeThresholdMs (one hour by default). GapHandlingTimeoutMs does not help on SQL Server, because only the PostgreSQL provider writes tombstones. This issue asks for tombstones on SQL Server too, with the same GapHandlingTimeoutMs option.
Problem
GlobalPosition is an IDENTITY column. SQL Server gives an identity value to an insert when the insert runs, and it does not give the value back when the transaction rolls back. These failures roll back after the insert, so each one leaves a gap:
- An append that breaks
UQ_StreamIdAndMessageId, for example an event that is written again with the same message ID. - Two appends to the same stream that read the same version in
check_stream(#597, fixed by #598 for appends to an existing stream). - A multi-stream append (
IEventWriter.AppendEventswith manyNewStreamAppend) where a later stream fails after an earlier stream inserted its rows.
The subscription cannot tell a rolled back insert from an append that did not commit yet, so it holds the gap. That is correct, because a skip could lose an event. But the options to release the gap on SQL Server have these problems:
| Option | Result on SQL Server |
|---|---|
GapAgeThresholdMs (default one hour) |
Every all-stream subscription stops for up to one hour. |
GapSkipTimeoutMs |
The gap is released sooner. An append that takes longer than the timeout to commit is skipped, and its events never reach the subscription. |
GapHandlingTimeoutMs |
No effect. SqlSubscriptionBase.HandleGapTimeout does nothing, and only PostgresSubscriptionBase overrides it. |
Reproduction
Eventuous 0.17.0, SQL Server 2022, a SqlServerAllStreamSubscription with the default options:
- Append an event with message ID
Xto streamA, and wait until the subscription handles it. - Append another event with message ID
Xto streamA, with the correct expected version. The append fails with error 2627 onUQ_StreamIdAndMessageId, and it uses one identity value. - Append an event to stream
B.
The subscription does not handle the event of stream B within 30 seconds. When step 2 fails with a wrong expected version instead (before any insert), the subscription handles the event of stream B within about 60 ms.
Proposal
Do for SQL Server what the PostgreSQL provider does in 9_TryInsertTombstone.sql with OVERRIDING SYSTEM VALUE. SQL Server writes an explicit identity value with SET IDENTITY_INSERT ... ON.
- Add a
try_insert_tombstoneprocedure to the SQL Server schema. It inserts a tombstone row at the gap position, in a tombstone stream. - Override
HandleGapTimeoutinSqlServerSubscriptionBaseto call the procedure. - Override
ShouldSkipEventinSqlServerSubscriptionBaseto skip the tombstone rows, as the PostgreSQL provider does.
DetectGap already gives remediation its chance before GapSkipTimeoutMs skips a gap, so no change to the shared polling code is necessary.
A sketch of the procedure (not tested):
CREATE OR ALTER PROCEDURE __schema__.try_insert_tombstone
@gap_position BIGINT,
@stream_name NVARCHAR(850),
@message_type NVARCHAR(128),
@message_id UNIQUEIDENTIFIER
WITH EXECUTE AS OWNER /* SET IDENTITY_INSERT needs ALTER permission on the table. */
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
SET LOCK_TIMEOUT 5000; /* Do not hold the polling loop while an open append holds the position. */
DECLARE @stream_id INT, @stream_position INT;
BEGIN TRY
BEGIN TRANSACTION;
/* The tombstone stream can also be created once by the schema script. */
IF NOT EXISTS (SELECT 1 FROM __schema__.Streams WHERE StreamName = @stream_name)
INSERT INTO __schema__.Streams (StreamName, [Version]) VALUES (@stream_name, -1);
UPDATE __schema__.Streams
SET @stream_position = [Version] = [Version] + 1,
@stream_id = StreamId
WHERE StreamName = @stream_name;
SET IDENTITY_INSERT __schema__.Messages ON;
INSERT INTO __schema__.Messages (
GlobalPosition, MessageId, MessageType, StreamId, StreamPosition, JsonData, JsonMetadata, Created
)
VALUES (
@gap_position, @message_id, @message_type, @stream_id, @stream_position, N'{}', N'{}', SYSUTCDATETIME()
);
SET IDENTITY_INSERT __schema__.Messages OFF;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
SET IDENTITY_INSERT __schema__.Messages OFF;
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
/*
2627, 2601: an append committed at the position, so the gap is not a gap.
1222: an open append still holds the position. The next poll tries again.
*/
IF ERROR_NUMBER() NOT IN (2627, 2601, 1222) THROW;
END CATCH;
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
END;
Why a tombstone does not take the position of a slow append
While an append is open, its uncommitted row holds an exclusive lock on its key in the clustered primary key. A tombstone insert at the same position must check the primary key, so it waits for that lock. The append then decides the result:
- When the append commits, the tombstone fails with a duplicate key, and the event stays.
- When the append rolls back, the tombstone fills the position.
I tested these four cases on SQL Server 2022 with a copy of the 0.17.0 Messages table (the same identity column, primary key, and unique constraints, with READ_COMMITTED_SNAPSHOT off), with SET IDENTITY_INSERT ON and SET LOCK_TIMEOUT 3000:
| Case | Result of the tombstone insert |
|---|---|
| The append is open | Waits, then fails with 1222 (lock timeout) |
| The append rolled back | Succeeds. IDENT_CURRENT does not change. |
| The append committed | Fails with 2627. The event stays. |
| The append commits while the tombstone waits | Fails with 2627. The event stays. |
So unlike GapSkipTimeoutMs, a tombstone cannot lose an event.
Open questions
- Permissions.
SET IDENTITY_INSERTneeds ALTER permission on the table, or ownership of the table. The sketch usesEXECUTE AS OWNER. Another way is a note in the docs that the subscription login needs ALTER permission onMessages. - One table per session. Only one table in a session can have
IDENTITY_INSERTon. The procedure turns it off in both paths, so a pooled connection does not keep it on. - Read committed snapshot. I did not test with
READ_COMMITTED_SNAPSHOTon. I expect the same result, because a uniqueness check takes locks and does not read row versions. - Shared constants. The tombstone stream name and message type (
__tombstones__,$tombstone) are inPostgresSubscriptionConstants. They could move toEventuous.Sql.Base, so both providers use the same names.
- Ngôn ngữ chính
- C#
- Star
- 512
- Fork
- 99
- Merge trung bình
- 8 ngày 16 giờ
- Pull request đã merge (30 ngày)
- 3
Chuẩn bị môi trường
- Có Dockerfile hoặc tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của Eventuous/eventuous
-
MongoDB integration tests fail on newer GitHub Actions runner kernelsCó thể đã có người làm @nmummau đã nhận 11 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
Eventuous/eventuous#626 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 7 ngày
-
`dotnet format style --verify-no-changes` fails with naming violations and unnecessary importsCó thể đã có người làm @nmummau đã nhận 11 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 80/100
Eventuous/eventuous#624 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 7 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
Eventuous/eventuous#627 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 7 ngày
-
SQL Server: `append_events` does not need the `@inserted` table variableCó thể đã có người làm @nmummau đã nhận 17 ngày trước. Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 20/100
Eventuous/eventuous#619 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 7 ngày
-
SQL Server: stream reads do one key lookup per event, and `IDX_EventsStream` duplicates `UQ_StreamIdAndStreamPosition`Có thể đã có người làm @nmummau đã nhận 17 ngày trước. Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 18/100
Eventuous/eventuous#618 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 7 ngày
Tất cả issue của Eventuous/eventuous
Issue tương tự
-
possible bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
-
Include host.wslc.internal in the HTTPS development certificateCó thể đã có người làm @afscrome đã nhận hôm nay. Đang mởarea-commandlinetools
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
dotnet/aspnetcore#70122 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
:1st_place_medal: good first issue :cowboy_hat_face: refactor :roller_coaster: enhancement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
ApplETS/Backend-Hello#78 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
Maintainer thường phản hồi trong vòng 2 ngày
-
Custom component in Summary2 no longer receives raw formData (regression from app-frontend v4)Đang mởstatus/triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
Altinn/altinn-studio#21642 ·
Maintainer thường phản hồi trong vòng 1 ngày