Skip to content

Replace BatchableSubscriber interface with batch attributes#865

Merged
DavidBadura merged 8 commits into
4.0.xfrom
feature/batch-subscriber-attributes
Jun 18, 2026
Merged

Replace BatchableSubscriber interface with batch attributes#865
DavidBadura merged 8 commits into
4.0.xfrom
feature/batch-subscriber-attributes

Conversation

@DavidBadura

@DavidBadura DavidBadura commented Jun 16, 2026

Copy link
Copy Markdown
Member

Batching for subscribers no longer relies on the BatchableSubscriber interface. The batch lifecycle is now declared with attributes, in line with the rest of the subscriber API which is already attribute and argument-resolver driven.

A subscriber opts into batching with #[BatchBegin], #[BatchFlush], #[BatchRollback] and optionally #[BatchShouldFlush]. The begin method may return a state object, which is injected into subscribe handlers via the #[BatchState] parameter attribute and passed to flush/rollback.

Subscribers no longer implement the BatchableSubscriber interface to take
part in batching. Instead the batch lifecycle is declared with attributes:
#[BatchBegin], #[BatchFlush], #[BatchRollback] and #[BatchShouldFlush], with
#[BatchState] to inject the per-batch state into a subscribe handler. This
keeps batching consistent with the rest of the subscriber API, which is
already attribute and argument-resolver driven.

The metadata factory now reads these into a BatchMetadata and rejects
duplicate or incomplete batch method sets. Argument resolution for handlers
moved into the MessageProcessor, and a BatchManager/Batch pair tracks the
open batch per subscription. Upgrade notes and the subscription docs are
updated accordingly.
ArgumentMetadata now carries the list of attribute instances attached to a
subscribe handler parameter, with an attribute() lookup helper, rather than a
dedicated bool for BatchState. The batch argument resolver and the metadata
factory ask for the BatchState attribute through that list, which keeps the
metadata generic for future parameter attributes.

Also fixes the nullsafe chain in BatchSubscriber, where metadata() was called
on a possibly-null accessor before the null check.
@github-actions

github-actions Bot commented Jun 16, 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 () | 3.523ms (±0.00%)   | 3.329ms (±0.00%)   | +5.82%    | 35.769mb        | 36.288mb   | -1.43%      |
| benchSave10000Events () | 506.958ms (±0.00%) | 508.059ms (±0.00%) | -0.22%    | 35.904mb        | 36.423mb   | -1.43%      |
+-------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

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

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad1Event ()                     | 1.222ms (±0.00%)   | 1.197ms (±0.00%)   | +2.11%    | 35.141mb        | 35.141mb   | 0.00%       |
| benchLoad10000Events ()                | 71.432ms (±0.00%)  | 73.984ms (±0.00%)  | -3.45%    | 35.141mb        | 35.141mb   | 0.00%       |
| benchSave1Event ()                     | 1.169ms (±0.00%)   | 1.112ms (±0.00%)   | +5.12%    | 35.141mb        | 35.141mb   | 0.00%       |
| benchSave10000Events ()                | 303.366ms (±0.00%) | 286.731ms (±0.00%) | +5.80%    | 35.141mb        | 35.141mb   | 0.00%       |
| benchSave10000Aggregates ()            | 7.552s (±0.00%)    | 7.574s (±0.00%)    | -0.29%    | 35.141mb        | 35.141mb   | 0.00%       |
| benchSave10000AggregatesTransaction () | 4.970s (±0.00%)    | 4.938s (±0.00%)    | +0.66%    | 35.141mb        | 35.141mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

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

+---------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                           | time (kde mode)                                     | memory                                     |
+---------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                   | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+---------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchHandle10000Events () | 105.972ms (±0.00%) | 100.823ms (±0.00%) | +5.11%    | 35.720mb        | 35.720mb   | 0.00%       |
+---------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

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

+---------------------------+--------------------+-------------------+-----------+-----------------+------------+-------------+
|                           | time (kde mode)                                    | memory                                     |
+---------------------------+--------------------+-------------------+-----------+-----------------+------------+-------------+
| subject                   | Tag: <current>     | Tag: base         | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+---------------------------+--------------------+-------------------+-----------+-----------------+------------+-------------+
| benchHandle10000Events () | 100.224ms (±0.00%) | 98.438ms (±0.00%) | +1.81%    | 48.296mb        | 48.296mb   | 0.00%       |
+---------------------------+--------------------+-------------------+-----------+-----------------+------------+-------------+

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

+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+
|                           | time (kde mode)                               | memory                                     |
+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+
| subject                   | Tag: <current>  | Tag: base       | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+
| benchHandle10000Events () | 3.280s (±0.00%) | 3.287s (±0.00%) | -0.23%    | 48.296mb        | 48.296mb   | 0.00%       |
+---------------------------+-----------------+-----------------+-----------+-----------------+------------+-------------+

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

+----------------+------------------+------------------+-----------+-----------------+------------+-------------+
|                | time (kde mode)                                 | memory                                     |
+----------------+------------------+------------------+-----------+-----------------+------------+-------------+
| subject        | Tag: <current>   | Tag: base        | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------+------------------+------------------+-----------+-----------------+------------+-------------+
| benchCreate () | 3.308ms (±0.00%) | 3.057ms (±0.00%) | +8.21%    | 5.062mb         | 4.881mb    | +3.70%      |
| benchUpdate () | 4.553ms (±0.00%) | 4.195ms (±0.00%) | +8.54%    | 5.052mb         | 5.067mb    | -0.29%      |
| benchBoth ()   | 7.371ms (±0.00%) | 7.089ms (±0.00%) | +3.98%    | 5.751mb         | 5.072mb    | +13.39%     |
+----------------+------------------+------------------+-----------+-----------------+------------+-------------+

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

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad1Event ()                     | 1.191ms (±0.00%)   | 1.225ms (±0.00%)   | -2.74%    | 35.805mb        | 35.805mb   | 0.00%       |
| benchLoad10000Events ()                | 100.394ms (±0.00%) | 101.119ms (±0.00%) | -0.72%    | 35.805mb        | 35.805mb   | 0.00%       |
| benchSave1Event ()                     | 2.109ms (±0.00%)   | 2.100ms (±0.00%)   | +0.42%    | 35.805mb        | 35.805mb   | 0.00%       |
| benchSave10000Events ()                | 325.386ms (±0.00%) | 326.606ms (±0.00%) | -0.37%    | 35.807mb        | 35.807mb   | 0.00%       |
| benchSave10000Aggregates ()            | 11.932s (±0.00%)   | 11.772s (±0.00%)   | +1.36%    | 35.805mb        | 35.805mb   | 0.00%       |
| benchSave10000AggregatesTransaction () | 9.209s (±0.00%)    | 9.232s (±0.00%)    | -0.24%    | 36.045mb        | 36.045mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

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

+----------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                   | memory                                     |
+----------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>    | Tag: base         | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+
| benchLoad10000EventsMissingSnapshot () | 73.058ms (±0.00%) | 73.303ms (±0.00%) | -0.33%    | 35.146mb        | 35.146mb   | 0.00%       |
| benchLoad10000Events ()                | 1.392ms (±0.00%)  | 1.434ms (±0.00%)  | -2.90%    | 35.145mb        | 35.145mb   | 0.00%       |
+----------------------------------------+-------------------+-------------------+-----------+-----------------+------------+-------------+

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

+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
|                                        | time (kde mode)                                     | memory                                     |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| subject                                | Tag: <current>     | Tag: base          | time-diff | Tag: <current>  | Tag: base  | memory-diff |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+
| benchLoad1Event ()                     | 1.246ms (±0.00%)   | 1.297ms (±0.00%)   | -3.92%    | 36.306mb        | 36.306mb   | 0.00%       |
| benchLoad10000Events ()                | 73.048ms (±0.00%)  | 75.197ms (±0.00%)  | -2.86%    | 36.306mb        | 36.306mb   | 0.00%       |
| benchSave1Event ()                     | 1.119ms (±0.00%)   | 1.228ms (±0.00%)   | -8.93%    | 36.306mb        | 36.306mb   | 0.00%       |
| benchSave10000Events ()                | 318.013ms (±0.00%) | 325.314ms (±0.00%) | -2.24%    | 36.306mb        | 36.306mb   | 0.00%       |
| benchSave10000Aggregates ()            | 7.580s (±0.00%)    | 7.548s (±0.00%)    | +0.42%    | 36.307mb        | 36.307mb   | 0.00%       |
| benchSave10000AggregatesTransaction () | 4.978s (±0.00%)    | 5.011s (±0.00%)    | -0.67%    | 36.307mb        | 36.307mb   | 0.00%       |
| benchAppend1Event ()                   | 1.528ms (±0.00%)   | 1.542ms (±0.00%)   | -0.86%    | 36.306mb        | 36.306mb   | 0.00%       |
| benchAppend100Events ()                | 7.376ms (±0.00%)   | 7.847ms (±0.00%)   | -6.00%    | 36.306mb        | 36.306mb   | 0.00%       |
+----------------------------------------+--------------------+--------------------+-----------+-----------------+------------+-------------+

This comment gets update everytime a new commit comes in!

The event emitter section passed argumentResolvers to
MetadataSubscriberAccessorRepository, which no longer takes them. Resolvers
are registered on the DefaultSubscriptionEngine, matching the upgrade guide.
The lookup resolver is not registered by default, so show how to pass it to
the subscription engine, the same as the event emitter resolver.
@DavidBadura
DavidBadura requested a review from DanielBadura June 16, 2026 17:24
Every caller only checks for presence, so a bool hasAttribute() reads better
than returning the attribute instance.
Add tests for ArgumentMetadata::hasAttribute, the usesBatching attribute
detection, message argument resolving in MessageProcessor and the
afterMessages flush path of the batch subscriber.
The string-concatenated cache key was fragile against key collisions
between the subscription id, event class and method name. A nested array
keyed by those three parts cannot collide and drops the brittle key
building.

@DanielBadura DanielBadura left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should adjust the infection.json msi values down to the current value with this PR

@DavidBadura
DavidBadura merged commit 135d68b into 4.0.x Jun 18, 2026
36 of 38 checks passed
@DavidBadura
DavidBadura deleted the feature/batch-subscriber-attributes branch June 18, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants