Skip to content

Add checkpoints for decision models - #927

Open
DavidBadura wants to merge 2 commits into
4.0.xfrom
decision-model-checkpoint
Open

DavidBadura wants to merge 2 commits into
4.0.xfrom
decision-model-checkpoint

Conversation

@DavidBadura

@DavidBadura DavidBadura commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Replaces #925, which reused #[SplitStream] for this. That gave the same attribute two meanings when aggregates and DCB share a store: for an aggregate it cuts off the whole stream, for DCB it would only have cut off projections that handle the event. So this adds a separate, explicit attribute on the projection instead and leaves #[SplitStream] untouched.

#[Apply]
#[Checkpoint]
public function applyGuestsCounted(int $state, GuestsCounted $event): int
{
    return $event->numberOfGuests;
}

The event of a checkpoint method has to contain the whole state the projection needs. When building a decision model:

  1. The builder looks up the last checkpoint event of every projection that has one, with the projection's own tags and stream, in a single onlyLastEvent query.
  2. It reads the normal query from the smallest of these positions (query($query, $from) from Allow reading events from a position with AppendStore::query() #924).
  3. Every projection only gets messages from its own checkpoint onwards.

The store doesn't know anything about checkpoints, nothing is archived and the append condition is unchanged. Events before a checkpoint are older than after anyway, and anything appended concurrently is newer. If one projection in the decision model has no checkpoint, the read starts at 0, so that case is correct but doesn't save anything yet. The same applies to StoreProjectionBuilder.

#[Checkpoint] without #[Apply] throws an ApplyMethodDetectionError.

This also adds DynamicConsistencyBoundaryBench. The existing taggable store benchmarks only cover aggregates and plain appends, but the cost of a decision depends on the size of the whole table and on how selective the tags are. The benchmark fills the store with 1.000 courses, 1.000 invoices and two courses with a long history (one of them with a checkpoint every 100 events) and measures small, composite and long decision models, checkpoints, onlyLastEvent and conditional appends against that.

A projection that has to load a long history can now mark an apply method
with #[Checkpoint]. Its event has to contain the whole state the projection
needs, so everything before the last of these events is skipped.

Before reading, the builders look up the last checkpoint event of every
projection with the projection's own tags and read from there. Each projection
only gets events from its own checkpoint onwards, so other projections in the
same decision model still get their whole history. Nothing is archived and the
append condition stays the same.

This is deliberately separate from #[SplitStream]: that one cuts off a whole
stream for aggregates, a checkpoint only affects the projection declaring it.
@DavidBadura DavidBadura added this to the 4.0.0 milestone Oct 3, 2026
@DavidBadura DavidBadura added the enhancement New feature or request label Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Hello 👋

here is the most recent benchmark result:

SplitStreamBench
================

+-------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                         | time (kde mode)                                     | memory                                     |
+-------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                 | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+-------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad10000Events () | 2.154ms (±0.00%)   | 2.328ms (±0.00%)   | -7.47%    | 35.774mb        | 36.295mb   | -1.43%      |
| benchSave10000Events () | 478.645ms (±0.00%) | 595.895ms (±0.00%) | -19.68%   | 35.909mb        | 35.909mb   | 0.00%       |
+-------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

SimpleSetupTaggableStoreBench
=============================

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad1Event ()                     | 1.217ms (±0.00%)   | 983.600μs (±0.00%) | +23.68%   | 36.312mb        | 36.312mb   | 0.00%       |
| benchLoad10000Events ()                | 56.536ms (±0.00%)  | 57.142ms (±0.00%)  | -1.06%    | 36.312mb        | 36.312mb   | 0.00%       |
| benchSave1Event ()                     | 1.348ms (±0.00%)   | 1.300ms (±0.00%)   | +3.72%    | 36.312mb        | 36.312mb   | 0.00%       |
| benchSave10000Events ()                | 297.785ms (±0.00%) | 325.598ms (±0.00%) | -8.54%    | 36.312mb        | 36.312mb   | 0.00%       |
| benchSave10000Aggregates ()            | 9.573s (±0.00%)    | 10.592s (±0.00%)   | -9.62%    | 36.312mb        | 36.312mb   | 0.00%       |
| benchSave10000AggregatesTransaction () | 4.714s (±0.00%)    | 4.688s (±0.00%)    | +0.56%    | 36.312mb        | 36.312mb   | 0.00%       |
| benchAppend1Event ()                   | 1.275ms (±0.00%)   | 1.300ms (±0.00%)   | -1.91%    | 36.312mb        | 36.312mb   | 0.00%       |
| benchAppend100Events ()                | 6.313ms (±0.00%)   | 7.847ms (±0.00%)   | -19.56%   | 36.312mb        | 36.312mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

SubscriptionEngineBench
=======================

+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+
|                           | time (kde mode)                               | memory                                     |
+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+
| subject                   | Tag: <current>  | Tag: base       | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+
| benchHandle10000Events () | 2.571s (±0.00%) | 2.520s (±0.00%) | +2.00%    | 48.301mb        | 48.301mb   | 0.00%       |
+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+

NoopSubscriptionEngineBench
===========================

+---------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
|                           | time (kde mode)                                   | memory                                     |
+---------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| subject                   | Tag: <current>    | Tag: base         | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+---------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| benchHandle10000Events () | 83.130ms (±0.00%) | 81.381ms (±0.00%) | +2.15%    | 48.301mb        | 48.301mb   | 0.00%       |
+---------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+

SimpleSetupStreamStoreBench
===========================

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad1Event ()                     | 1.093ms (±0.00%)   | 1.076ms (±0.00%)   | +1.53%    | 35.146mb        | 35.146mb   | 0.00%       |
| benchLoad10000Events ()                | 57.444ms (±0.00%)  | 56.709ms (±0.00%)  | +1.30%    | 35.146mb        | 35.146mb   | 0.00%       |
| benchSave1Event ()                     | 1.249ms (±0.00%)   | 1.212ms (±0.00%)   | +3.07%    | 35.146mb        | 35.146mb   | 0.00%       |
| benchSave10000Events ()                | 248.476ms (±0.00%) | 258.283ms (±0.00%) | -3.80%    | 35.146mb        | 35.146mb   | 0.00%       |
| benchSave10000Aggregates ()            | 12.941s (±0.00%)   | 7.879s (±0.00%)    | +64.26%   | 35.146mb        | 35.146mb   | 0.00%       |
| benchSave10000AggregatesTransaction () | 4.665s (±0.00%)    | 4.673s (±0.00%)    | -0.16%    | 35.147mb        | 35.147mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

SubscriptionEngineBatchBench
============================

+---------------------------+-------------------+--------------------+-----------+-----------------+------------+-------------+
|                           | time (kde mode)                                    | memory                                     |
+---------------------------+-------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                   | Tag: <current>    | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+---------------------------+-------------------+--------------------+-----------+-----------------+------------+-------------+
| benchHandle10000Events () | 86.791ms (±0.00%) | 119.548ms (±0.00%) | -27.40%   | 35.726mb        | 35.726mb   | 0.00%       |
+---------------------------+-------------------+--------------------+-----------+-----------------+------------+-------------+

DynamicConsistencyBoundaryBench
===============================

+------------------------------------------------+-------------------+-----------+-----------------+-------------+
|                                                | time (kde mode)               | memory                        |
+------------------------------------------------+-------------------+-----------+-----------------+-------------+
| subject                                        | Tag: <current>    | time-diff | Tag: <current>  | memory-diff |
+------------------------------------------------+-------------------+-----------+-----------------+-------------+
| benchDecisionModelSmall ()                     | 5.922ms (±0.00%)  | +0.00%    | 13.861mb        | +0.00%      |
| benchDecisionModelComposite ()                 | 18.956ms (±0.00%) | +0.00%    | 13.861mb        | +0.00%      |
| benchDecisionModel10000Events ()               | 75.551ms (±0.00%) | +0.00%    | 13.861mb        | +0.00%      |
| benchDecisionModel10000EventsWithCheckpoint () | 13.751ms (±0.00%) | +0.00%    | 13.861mb        | +0.00%      |
| benchDecisionModelOnlyLastEvent ()             | 3.005ms (±0.00%)  | +0.00%    | 13.861mb        | +0.00%      |
| benchDecideAndAppend ()                        | 30.478ms (±0.00%) | +0.00%    | 13.861mb        | +0.00%      |
| benchAppendWithCondition ()                    | 10.501ms (±0.00%) | +0.00%    | 13.861mb        | +0.00%      |
+------------------------------------------------+-------------------+-----------+-----------------+-------------+

PersonalDataBench
=================

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad1Event ()                     | 708.600μs (±0.00%) | 722.200μs (±0.00%) | -1.88%    | 37.008mb        | 37.008mb   | +0.00%      |
| benchLoad10000Events ()                | 134.276ms (±0.00%) | 133.768ms (±0.00%) | +0.38%    | 37.008mb        | 37.008mb   | +0.00%      |
| benchSave1Event ()                     | 1.711ms (±0.00%)   | 1.895ms (±0.00%)   | -9.69%    | 37.008mb        | 37.008mb   | 0.00%       |
| benchSave10000Events ()                | 354.276ms (±0.00%) | 369.162ms (±0.00%) | -4.03%    | 37.063mb        | 37.063mb   | -0.00%      |
| benchSave10000Aggregates ()            | 15.453s (±0.00%)   | 14.256s (±0.00%)   | +8.39%    | 37.008mb        | 37.008mb   | 0.00%       |
| benchSave10000AggregatesTransaction () | 8.591s (±0.00%)    | 8.565s (±0.00%)    | +0.30%    | 37.008mb        | 37.008mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

SnapshotsBench
==============

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad10000EventsMissingSnapshot () | 55.723ms (±0.00%)  | 57.098ms (±0.00%)  | -2.41%    | 35.151mb        | 35.151mb   | 0.00%       |
| benchLoad10000Events ()                | 881.000μs (±0.00%) | 899.600μs (±0.00%) | -2.07%    | 35.151mb        | 35.151mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

CommandToQueryBench
===================

+----------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
|                | time (kde mode)                                   | memory                                     |
+----------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| subject        | Tag: <current>    | Tag: base         | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| benchCreate () | 3.000ms (±0.00%)  | 3.243ms (±0.00%)  | -7.49%    | 5.057mb         | 5.057mb    | 0.00%       |
| benchUpdate () | 4.177ms (±0.00%)  | 15.199ms (±0.00%) | -72.52%   | 5.067mb         | 5.067mb    | 0.00%       |
| benchBoth ()   | 10.186ms (±0.00%) | 7.130ms (±0.00%)  | +42.86%   | 5.389mb         | 5.389mb    | 0.00%       |
+----------------+-------------------+-------------------+-----------+-----------------+------------+-------------+

This comment gets update everytime a new commit comes in!

The existing taggable store benchmarks only cover aggregates and plain
appends. Decisions are different: their cost depends on the size of the whole
table and on how selective the tags are, not just on the matching events.

The new benchmark fills the store with 1.000 courses, 1.000 invoices and two
courses with a long history, and measures building small, composite and long
decision models, checkpoints, only-last-event projections and conditional
appends against that data.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant