Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

MongoCheckpointStore leaks a batching pipeline on every GetLastCheckpoint call

未关闭
#616 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 7 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
48/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
技术栈
csharp, mongodb
领域
backend, database

调研方向

Start with src/Mongo/src/Eventuous.Projections.MongoDB/MongoCheckpointStore.cs and trace how GetLastCheckpoint, StoreCheckpoint, and GetSubject handle subjects, batching, and errors. Then read EventSubscriptionWithCheckpoint.GetCheckpoint and the checkpoint store registration to understand call frequency and lifetime. Add a regression test for repeated reads or resubscription, then verify that checkpoint writes remain ordered and failures do not stop later writes.

由索引模型根据 Issue 内容生成。

描述

bug mongo

MongoCheckpointStore.GetLastCheckpoint is not a pure read. Every call builds a new Subject with its own Rx batching pipeline and overwrites the previous one for that checkpoint ID, without completing or disposing it:

public async ValueTask<EventuousCheckpoint> GetLastCheckpoint(string checkpointId, CancellationToken cancellationToken = default) {
    // ... read the stored checkpoint ...
    _subjects[checkpointId] = GetSubject();   // replaces whatever was there

    return checkpoint;
}

readonly Dictionary<string, Subject<EventuousCheckpoint>> _subjects = new();

Subject<EventuousCheckpoint> GetSubject() {
    var subject = new Subject<EventuousCheckpoint>();
    // ... Buffer(interval, size) by default ...
    observable
        .Where(x => x.Count > 0)
        .Select(x => Observable.FromAsync(ct => StoreInternal(x.Last(), false, ct)))
        .Concat()
        .Subscribe();                          // IDisposable discarded, no onError

    return subject;
}

MongoCheckpointStore.cs#L32-L66

GetLastCheckpoint is called once per subscription run (EventSubscriptionWithCheckpoint.GetCheckpoint), so this happens on every resubscribe, not only at startup.

Found by reading the code while reviewing #603. None of it is reproduced with a failing test yet.

Consequences

1. One leaked pipeline per resubscribe. The old subject is dropped from the dictionary, but its pipeline is never completed and the IDisposable returned by Subscribe() is thrown away. With the default options (BatchSize = 1, BatchIntervalSec = 5) the pipeline is Buffer(TimeSpan, int), which runs on a timer, so each abandoned pipeline stays alive and wakes every 5 seconds for the rest of the process lifetime. A subscription that drops and recovers N times carries N orphaned timers.

2. Writes from the old and the new pipeline are not ordered against each other. Concat() serialises writes within one pipeline only. Whatever is still buffered or in flight on the old subject is written when its timer or queued write fires, which can be after the replacement run has stored a later position. The stored checkpoint then moves backwards until the next write. In production the window is narrow, because there is one replacement per resubscribe. It was much wider in the MongoDB projection tests, which polled GetLastCheckpoint every 100 ms against a live subscription until #603 changed them to read the collection directly.

3. _subjects is a plain Dictionary on a singleton. AddCheckpointStore<T>() registers the store as a singleton shared by every subscription in the container. GetLastCheckpoint writes the dictionary (L42) while other subscriptions' commit handlers read it in StoreCheckpoint (L75). One subscription resubscribing while another commits is an unsynchronised write and read.

Related, in the same pipeline

.Subscribe() has no error handler (L63). If StoreInternal throws, for example on a transient MongoDB failure, the sequence terminates and later OnNext calls on that subject go nowhere. Non-forced checkpoints then stop being persisted until the next resubscribe replaces the subject, or until shutdown force-writes the last position.

CheckpointCommitHandler cannot see this. StoreCheckpoint(force: false) returns as soon as OnNext does, so the handler records the position as committed and its UnableToCommitPosition log never fires.

Suggested direction

  • Make GetLastCheckpoint a pure read. Create the subject lazily, once per checkpoint ID (ConcurrentDictionary.GetOrAdd on the first non-forced store), so there is one pipeline per ID for the lifetime of the store.
  • Give each write its own error handling (catch, log, continue) so one failed write does not end the pipeline.
  • Route forced writes through the same serialisation as batched ones, so a forced write cannot race a batched write that is still in flight.
  • Implement IAsyncDisposable on the store to complete the subjects and flush on shutdown.

Alternatively, consider dropping store-level batching. CheckpointCommitHandler already batches by CheckpointCommitBatchSize and CheckpointCommitDelayMs before it calls the store, so BatchSize and BatchIntervalSec batch a second time.

主要语言
C#
星标
512
派生
99
平均合并
5 天 6 小时
30 天内合并 PR
5

环境准备

  • 提供 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

Eventuous/eventuous 的其他 Issue

查看 Eventuous/eventuous 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。