Passive and inactive reducers
Use a passive read model when you want to reconstruct state from events on demand instead of maintaining a sink. Disabling a reducer’s activity is a different choice: it stops observation, but does not change how the model is read.
Choose the read behavior
Section titled “Choose the read behavior”| Configuration | Active observation | Sink | By-id read |
|---|---|---|---|
| Default | Enabled | Configured sink | Current materialized state, eventually consistent |
[Reducer(isActive: false)] only | Disabled | Still configured | Materialized state; it may be absent or stale |
[Passive] on the read model | Disabled | None | Reconstruct from the matching events |
For a self reducer, the read model is also the reducer declaration, so place [Passive] on that same type. For a classic reducer, place it on the target model, not the reducer service.
On-demand reads
Section titled “On-demand reads”Mark the read model [Passive]:
using Cratis.Chronicle.Events;using Cratis.Chronicle.ReadModels;using Cratis.Chronicle.Reducers;
[EventType]public record PassiveReducersTransactionCompleted(decimal Amount);
[Passive]public record PassiveReducersAdHocReport( decimal TotalRevenue, int TransactionCount, DateTimeOffset GeneratedAt);
public class PassiveReducersAdHocReportReducer : IReducerFor<PassiveReducersAdHocReport>{ public PassiveReducersAdHocReport Completed(PassiveReducersTransactionCompleted @event, PassiveReducersAdHocReport? current, EventContext context) { var revenue = current?.TotalRevenue ?? 0m; var count = current?.TransactionCount ?? 0;
return new PassiveReducersAdHocReport( revenue + @event.Amount, count + 1, context.Occurred); }}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule PassiveReducersTransactionCompleted do use Chronicle.Events.EventType, id: "passive-reducers-transaction-completed"
defstruct [:amount]end
defmodule PassiveReducersAdHocReport do use Chronicle.ReadModels.ReadModel
defstruct total_revenue: 0, transaction_count: 0, generated_at: nilend
# active: false makes the reducer passive: the read model is computed on demand# instead of being kept up to date in the background.defmodule PassiveReducersAdHocReportReducer do use Chronicle.Reducers.Reducer, model: PassiveReducersAdHocReport, active: false
@handles PassiveReducersTransactionCompleted
@impl true def reduce(%PassiveReducersTransactionCompleted{} = event, model, context) do revenue = if model, do: model.total_revenue, else: 0 count = if model, do: model.transaction_count, else: 0
struct!(PassiveReducersAdHocReport, total_revenue: revenue + event.amount, transaction_count: count + 1, generated_at: Map.get(context, :occurred) ) endendimport { EventContext, eventType, reducer } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class PassiveReducersTransactionCompleted { @field(Number) readonly amount: number;
constructor(amount: number) { this.amount = amount; }}
class PassiveReducersAdHocReport { totalRevenue = 0; transactionCount = 0; generatedAt = new Date();}
@reducer('', undefined, PassiveReducersAdHocReport, false)class PassiveReducersAdHocReportReducer { passiveReducersTransactionCompleted( event: PassiveReducersTransactionCompleted, current: PassiveReducersAdHocReport | undefined, context: EventContext ): PassiveReducersAdHocReport { const revenue = current?.totalRevenue ?? 0; const count = current?.transactionCount ?? 0;
return { totalRevenue: revenue + event.amount, transactionCount: count + 1, generatedAt: context.occurred }; }}This disables active observation regardless of the reducer’s isActive setting and selects on-demand reads through IReadModels. A sink collection cannot reconstruct passive state.
using Cratis.Chronicle;using Cratis.Chronicle.ReadModels;
public class PassiveReducersReportingService(IEventStore eventStore){ public async Task<PassiveReducersMonthlyRevenueReport> GenerateReport(Guid reportId) => // This triggers the passive reducer to compute state from events await eventStore.ReadModels.GetInstanceById<PassiveReducersMonthlyRevenueReport>(reportId);}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.IEventStore
class PassiveReducersReportingService(private val eventStore: IEventStore) { // This triggers the passive reducer to compute state from events suspend fun generateReport(reportId: String): PassiveReducersMonthlyRevenueReport? = eventStore.readModels.getInstanceByKey(PassiveReducersMonthlyRevenueReport::class, reportId)}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.EventStore;import io.cratis.chronicle.java.ReadModelsJavaBridge;
class PassiveReducersReportingService { private final EventStore eventStore;
PassiveReducersReportingService(EventStore eventStore) { this.eventStore = eventStore; }
// This triggers the passive reducer to compute state from events PassiveReducersMonthlyRevenueReport generateReport(String reportId) { return ReadModelsJavaBridge.getInstanceByKey( eventStore.getReadModels(), PassiveReducersMonthlyRevenueReport.class, reportId); }}defmodule PassiveReducersReportingService do # This triggers the passive reducer to compute state from events on demand. def generate_report(report_id) do Chronicle.read_model(PassiveReducersMonthlyRevenueReport, report_id) endendimport { IEventStore } from '@cratis/chronicle';
class PassiveReducersReportingService { constructor(private readonly store: IEventStore) {}
// This triggers the passive reducer to compute state from events generateReport(reportId: string): Promise<PassiveReducersMonthlyRevenueReport | null> { return this.store.readModels.findInstanceById(PassiveReducersMonthlyRevenueReport, reportId); }}No matching history, or a history whose events are all filtered out, returns absent state. A null reducer result deletes state; a later accepted event can recreate it. Source/stream filters and tag filters apply to both classic and self passive reads with the same OR-within-tags and AND-across-categories semantics as active delivery.
Disable observation only
Section titled “Disable observation only”Use isActive: false when you want the observer registered but not running:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Reducers;
[EventType]public record PassiveReducersDataRecorded(decimal Value);
public record PassiveReducersAnalytics(int RecordCount, decimal TotalValue, DateTimeOffset LastUpdated);
[Reducer(isActive: false)]public class PassiveReducersTemporaryAnalyticsReducer : IReducerFor<PassiveReducersAnalytics>{ public PassiveReducersAnalytics Recorded(PassiveReducersDataRecorded @event, PassiveReducersAnalytics? current, EventContext context) { var count = current?.RecordCount ?? 0; var sum = current?.TotalValue ?? 0m;
return new PassiveReducersAnalytics(count + 1, sum + @event.Value, context.Occurred); }}import io.cratis.chronicle.events.EventContextimport io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModelimport java.time.Instant
@EventTypedata class PassiveReducersDataRecorded(val value: Double)
@ReadModeldata class PassiveReducersAnalytics( val recordCount: Int = 0, val totalValue: Double = 0.0, val lastUpdated: Instant = Instant.EPOCH)
@Reducer(isActive = false)class PassiveReducersTemporaryAnalyticsReducer { fun recorded( event: PassiveReducersDataRecorded, current: PassiveReducersAnalytics?, context: EventContext ): PassiveReducersAnalytics = PassiveReducersAnalytics( recordCount = (current?.recordCount ?: 0) + 1, totalValue = (current?.totalValue ?: 0.0) + event.value, lastUpdated = context.occurred )}import io.cratis.chronicle.events.EventContext;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;import java.time.Instant;
@EventTyperecord PassiveReducersDataRecorded(double value) {}
@ReadModelrecord PassiveReducersAnalytics(int recordCount, double totalValue, Instant lastUpdated) { PassiveReducersAnalytics() { this(0, 0.0, Instant.EPOCH); }}
@Reducer(isActive = false)class PassiveReducersTemporaryAnalyticsReducer { PassiveReducersAnalytics recorded( PassiveReducersDataRecorded event, PassiveReducersAnalytics current, EventContext context) { int count = current == null ? 0 : current.recordCount(); double sum = current == null ? 0.0 : current.totalValue(); return new PassiveReducersAnalytics(count + 1, sum + event.value(), context.getOccurred()); }}defmodule PassiveReducersDataRecorded do use Chronicle.Events.EventType, id: "passive-reducers-data-recorded"
defstruct [:value]end
defmodule PassiveReducersAnalytics do defstruct record_count: 0, total_value: 0, last_updated: nilend
defmodule PassiveReducersTemporaryAnalyticsReducer do use Chronicle.Reducers.Reducer, model: PassiveReducersAnalytics, active: false
alias PassiveReducersDataRecorded
@handles PassiveReducersDataRecorded
@impl true def reduce(%PassiveReducersDataRecorded{} = event, current, context) do count = if current, do: current.record_count, else: 0 total = if current, do: current.total_value, else: 0
%PassiveReducersAnalytics{ record_count: count + 1, total_value: total + event.value, last_updated: Map.get(context, :occurred) } endendimport { EventContext, eventType, reducer } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class PassiveReducersDataRecorded { @field(Number) readonly value: number;
constructor(value: number) { this.value = value; }}
class PassiveReducersAnalytics { recordCount = 0; totalValue = 0; lastUpdated = new Date();}
// isActive: false — registered with the Kernel but does not automatically observe events@reducer('', undefined, PassiveReducersAnalytics, false)class PassiveReducersTemporaryAnalyticsReducer { passiveReducersDataRecorded( event: PassiveReducersDataRecorded, current: PassiveReducersAnalytics | undefined, context: EventContext ): PassiveReducersAnalytics { const count = current?.recordCount ?? 0; const sum = current?.totalValue ?? 0;
return { recordCount: count + 1, totalValue: sum + event.value, lastUpdated: context.occurred }; }}This does not make the model passive. Previously persisted state remains in its configured sink and is no longer maintained. By-id reads do not switch to event reconstruction unless the read model carries [Passive].
Bounded analysis, not arbitrary-date history
Section titled “Bounded analysis, not arbitrary-date history”A bounded GetInstances(eventCount) reconstructs from a prefix of event-type-selected events. The prefix is taken before tag/source/stream metadata filtering; rejected events do not cause Chronicle to read farther to find N accepted events.
using Cratis.Chronicle;using Cratis.Chronicle.Events;using Cratis.Chronicle.ReadModels;
[EventType]public record PassivePrefixAmountRecorded(decimal Amount);
[Passive]public record PassiveReducersAccountBalance(decimal Balance){ public static PassiveReducersAccountBalance Reduce(PassivePrefixAmountRecorded @event, PassiveReducersAccountBalance? current) => new((current?.Balance ?? 0m) + @event.Amount);}
public class PassiveReducersPrefixAnalysis(IEventStore eventStore){ public Task<IEnumerable<PassiveReducersAccountBalance>> GetBalancesFromPrefix(EventCount eventCount) => eventStore.ReadModels.GetInstances<PassiveReducersAccountBalance>(eventCount);}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.IEventStoreimport io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class PassiveReducersMoneyDeposited(val amount: Double = 0.0)
@ReadModeldata class PassiveReducersAccountBalance(val balance: Double = 0.0)
// Passive: nothing is stored; each read folds the account's events in the client.@Reducer(isActive = false)class PassiveReducersAccountBalanceReducer { fun deposited(event: PassiveReducersMoneyDeposited, state: PassiveReducersAccountBalance?) = PassiveReducersAccountBalance((state?.balance ?: 0.0) + event.amount)}
class PassiveReducersHistoricalBalanceService(private val eventStore: IEventStore) { // Returns the balance as of now. The JVM client has no read-as-of-a-date call; for past states, // read the snapshots with getSnapshotsById and pick the one that occurred before the date. suspend fun getCurrentBalance(accountId: String): PassiveReducersAccountBalance? = eventStore.readModels.getInstanceByKey(PassiveReducersAccountBalance::class, accountId)}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventStore;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord PassiveReducersMoneyDeposited(double amount) {}
@ReadModelrecord PassiveReducersAccountBalance(double balance) { PassiveReducersAccountBalance() { this(0.0); }}
// Passive: nothing is stored; each read folds the account's events in the client.@Reducer(isActive = false)class PassiveReducersAccountBalanceReducer { public PassiveReducersAccountBalance deposited( PassiveReducersMoneyDeposited event, PassiveReducersAccountBalance state) { double current = state != null ? state.balance() : 0.0; return new PassiveReducersAccountBalance(current + event.amount()); }}
class PassiveReducersHistoricalBalanceService { private final BlockingEventStore eventStore;
PassiveReducersHistoricalBalanceService(BlockingEventStore eventStore) { this.eventStore = eventStore; }
// Returns the balance as of now. The JVM client has no read-as-of-a-date call; for past states, // read the snapshots with ReadModelsJavaBridge.getSnapshotsById and pick the one that occurred // before the date. PassiveReducersAccountBalance getCurrentBalance(String accountId) { return eventStore.getReadModels().getInstanceByKey(PassiveReducersAccountBalance.class, accountId); }}defmodule PassiveReducersAccountBalance do defstruct balance: 0end
defmodule PassiveReducersHistoricalBalanceService do # A passive reducer computes state on-demand from the full history of # events — Chronicle.read_model/2 triggers that computation. def get_balance_at_date(account_id, _date) do Chronicle.read_model(PassiveReducersAccountBalance, account_id) endendimport { IEventStore } from '@cratis/chronicle';
class PassiveReducersAccountBalance { balance = 0;}
class PassiveReducersHistoricalBalanceService { constructor(private readonly store: IEventStore) {}
// Passive reducer computes state on-demand from historical events getBalanceAtDate(accountId: string): Promise<PassiveReducersAccountBalance | null> { return this.store.readModels.findInstanceById(PassiveReducersAccountBalance, accountId); }}This is not a date-based snapshot query. GetSnapshotsById currently returns no reducer history; current materialized snapshots and historical projection snapshots are different read surfaces.
Reports and exploratory analysis
Section titled “Reports and exploratory analysis”Passive models fit infrequent queries whose reconstruction cost is acceptable:
using Cratis.Chronicle.Events;using Cratis.Chronicle.ReadModels;using Cratis.Chronicle.Reducers;
[EventType]public record PassiveReducersPaymentReceived(string Category, decimal Amount);
[Passive]public record PassiveReducersMonthlyRevenueReport( decimal TotalRevenue, Dictionary<string, decimal> RevenueByCategory, int Month, int Year);
public class PassiveReducersMonthlyRevenueReportReducer : IReducerFor<PassiveReducersMonthlyRevenueReport>{ public PassiveReducersMonthlyRevenueReport Received(PassiveReducersPaymentReceived @event, PassiveReducersMonthlyRevenueReport? current, EventContext context) { var revenue = current?.TotalRevenue ?? 0m; var byCategory = current?.RevenueByCategory ?? new Dictionary<string, decimal>();
if (!byCategory.ContainsKey(@event.Category)) byCategory[@event.Category] = 0;
byCategory[@event.Category] += @event.Amount;
return new PassiveReducersMonthlyRevenueReport( revenue + @event.Amount, byCategory, context.Occurred.Month, context.Occurred.Year); }}import io.cratis.chronicle.events.EventContextimport io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class PassiveReducersPaymentReceived(val category: String, val amount: Double)
@ReadModeldata class PassiveReducersMonthlyRevenueReport( val totalRevenue: Double = 0.0, val revenueByCategory: Map<String, Double> = emptyMap(), val month: Int = 0, val year: Int = 0)
@Reducer(isActive = false)class PassiveReducersMonthlyRevenueReportReducer { fun received(event: PassiveReducersPaymentReceived, current: PassiveReducersMonthlyRevenueReport?, context: EventContext): PassiveReducersMonthlyRevenueReport { val revenue = current?.totalRevenue ?: 0.0 val byCategory = current?.revenueByCategory ?: emptyMap() val updatedCategory = (byCategory[event.category] ?: 0.0) + event.amount val zoned = context.occurred.atZone(java.time.ZoneOffset.UTC)
return PassiveReducersMonthlyRevenueReport( revenue + event.amount, byCategory + (event.category to updatedCategory), zoned.monthValue, zoned.year ) }}import io.cratis.chronicle.events.EventContext;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
import java.time.ZoneOffset;import java.time.ZonedDateTime;import java.util.HashMap;import java.util.Map;
@EventTyperecord PassiveReducersPaymentReceived(String category, double amount) {}
@ReadModelrecord PassiveReducersMonthlyRevenueReport( double totalRevenue, Map<String, Double> revenueByCategory, int month, int year) { PassiveReducersMonthlyRevenueReport() { this(0.0, Map.of(), 0, 0); }}
@Reducer(isActive = false)class PassiveReducersMonthlyRevenueReportReducer { PassiveReducersMonthlyRevenueReport received(PassiveReducersPaymentReceived event, PassiveReducersMonthlyRevenueReport current, EventContext context) { double revenue = current == null ? 0.0 : current.totalRevenue(); Map<String, Double> byCategory = new HashMap<>(current == null ? Map.of() : current.revenueByCategory()); byCategory.merge(event.category(), event.amount(), Double::sum);
ZonedDateTime occurred = context.getOccurred().atZone(ZoneOffset.UTC);
return new PassiveReducersMonthlyRevenueReport( revenue + event.amount(), byCategory, occurred.getMonthValue(), occurred.getYear()); }}defmodule PassiveReducersPaymentReceived do use Chronicle.Events.EventType, id: "passive-reducers-payment-received"
defstruct [:category, :amount]end
defmodule PassiveReducersMonthlyRevenueReport do defstruct total_revenue: 0, revenue_by_category: %{}, month: nil, year: nilend
defmodule PassiveReducersMonthlyRevenueReportReducer do use Chronicle.Reducers.Reducer, model: PassiveReducersMonthlyRevenueReport, active: false
alias PassiveReducersPaymentReceived
@handles PassiveReducersPaymentReceived
@impl true def reduce(%PassiveReducersPaymentReceived{} = event, current, context) do revenue = if current, do: current.total_revenue, else: 0 by_category = if current, do: current.revenue_by_category, else: %{}
updated_by_category = Map.update(by_category, event.category, event.amount, &(&1 + event.amount))
{:ok, occurred, _} = DateTime.from_iso8601(Map.get(context, :occurred))
%PassiveReducersMonthlyRevenueReport{ total_revenue: revenue + event.amount, revenue_by_category: updated_by_category, month: occurred.month, year: occurred.year } endendimport { EventContext, eventType, reducer } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class PassiveReducersPaymentReceived { @field(String) readonly category: string; @field(Number) readonly amount: number;
constructor(category: string, amount: number) { this.category = category; this.amount = amount; }}
class PassiveReducersMonthlyRevenueReport { totalRevenue = 0; revenueByCategory: Record<string, number> = {}; month = 0; year = 0;}
@reducer('', undefined, PassiveReducersMonthlyRevenueReport, false)class PassiveReducersMonthlyRevenueReportReducer { passiveReducersPaymentReceived( event: PassiveReducersPaymentReceived, current: PassiveReducersMonthlyRevenueReport | undefined, context: EventContext ): PassiveReducersMonthlyRevenueReport { const revenue = current?.totalRevenue ?? 0; const byCategory = { ...(current?.revenueByCategory ?? {}) };
byCategory[event.category] = (byCategory[event.category] ?? 0) + event.amount;
return { totalRevenue: revenue + event.amount, revenueByCategory: byCategory, month: context.occurred.getMonth() + 1, year: context.occurred.getFullYear() }; }}An exploratory model can declare a passive state shape such as this; supply its reducer before reading it:
using Cratis.Chronicle.ReadModels;
[Passive]public record PassiveReducersCustomerBehaviorAnalysis( int UniqueCustomers, decimal AverageOrderValue, Dictionary<int, int> OrdersByHour);import io.cratis.chronicle.events.EventContextimport io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModelimport java.time.Instant
@EventTypedata class PassiveReducersTransactionCompleted(val amount: Double)
@ReadModeldata class PassiveReducersAdHocReport( val totalRevenue: Double = 0.0, val transactionCount: Int = 0, val generatedAt: Instant = Instant.EPOCH)
@Reducer(isActive = false)class PassiveReducersAdHocReportReducer { fun completed(event: PassiveReducersTransactionCompleted, current: PassiveReducersAdHocReport?, context: EventContext): PassiveReducersAdHocReport { val revenue = current?.totalRevenue ?: 0.0 val count = current?.transactionCount ?: 0
return PassiveReducersAdHocReport(revenue + event.amount, count + 1, context.occurred) }}import io.cratis.chronicle.events.EventContext;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
import java.time.Instant;
@EventTyperecord PassiveReducersTransactionCompleted(double amount) {}
@ReadModelrecord PassiveReducersAdHocReport(double totalRevenue, int transactionCount, Instant generatedAt) { PassiveReducersAdHocReport() { this(0.0, 0, Instant.EPOCH); }}
@Reducer(isActive = false)class PassiveReducersAdHocReportReducer { PassiveReducersAdHocReport completed(PassiveReducersTransactionCompleted event, PassiveReducersAdHocReport current, EventContext context) { double revenue = current == null ? 0.0 : current.totalRevenue(); int count = current == null ? 0 : current.transactionCount();
return new PassiveReducersAdHocReport(revenue + event.amount(), count + 1, context.getOccurred()); }}Elixir does not support this workflow yet.import { reducer } from '@cratis/chronicle';
class PassiveReducersCustomerBehaviorAnalysis { uniqueCustomers = 0; averageOrderValue = 0; ordersByHour: Record<number, number> = {};}
@reducer('', undefined, PassiveReducersCustomerBehaviorAnalysis, false)class PassiveReducersCustomerBehaviorAnalysisReducer {}A large event-source history is folded again on each on-demand read. Prefer a materialized reducer for frequent reads, consider application caching where appropriate, and measure reconstruction cost rather than assuming on-demand is cheaper.
Changing activity
Section titled “Changing activity”For development, disable observation without promising on-demand reads:
using Cratis.Chronicle.Reducers;
public record PassiveReducersExperimentalMetrics(int SampleCount);
#if DEBUG[Reducer(isActive: false)]#endifpublic class PassiveReducersExperimentalMetricsReducer : IReducerFor<PassiveReducersExperimentalMetrics>;// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModel
@ReadModeldata class PassiveReducersExperimentalMetrics(val sampleCount: Int = 0)
// Registered but not run while a metric is still being worked out - the read model is only// produced on demand until you are ready to make it active.@Reducer(isActive = false)class PassiveReducersExperimentalMetricsReducer { fun on(event: PassiveReducersMetricRecorded) = PassiveReducersExperimentalMetrics(event.sampleCount)}
@EventTypedata class PassiveReducersMetricRecorded(val sampleCount: Int = 0)// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
@ReadModelrecord PassiveReducersExperimentalMetrics(int sampleCount) { PassiveReducersExperimentalMetrics() { this(0); }}
// Registered but not run while a metric is still being worked out - the read model is only// produced on demand until you are ready to make it active.@Reducer(isActive = false)class PassiveReducersExperimentalMetricsReducer { public PassiveReducersExperimentalMetrics on(PassiveReducersMetricRecorded event) { return new PassiveReducersExperimentalMetrics(event.sampleCount()); }}
@EventTyperecord PassiveReducersMetricRecorded(int sampleCount) {}defmodule PassiveReducersExperimentalMetricRecorded do use Chronicle.Events.EventType, id: "passive-reducers-experimental-metric-recorded"
defstruct [:value]end
defmodule PassiveReducersExperimentalMetrics do defstruct sample_count: 0end
defmodule PassiveReducersExperimentalMetricsReducer do # Mix.env/0 is evaluated at compile time, so a release build always sees # `active: true` — only local :dev builds register without activating. use Chronicle.Reducers.Reducer, model: PassiveReducersExperimentalMetrics, active: Mix.env() != :dev
alias PassiveReducersExperimentalMetricRecorded
@handles PassiveReducersExperimentalMetricRecorded
@impl true def reduce(%PassiveReducersExperimentalMetricRecorded{}, current, _context) do count = if current, do: current.sample_count, else: 0 %PassiveReducersExperimentalMetrics{sample_count: count + 1} endendimport { reducer } from '@cratis/chronicle';
class PassiveReducersExperimentalMetrics { sampleCount = 0;}
// Kept passive while experimental - flip isActive to true once the metric is trusted@reducer('', undefined, PassiveReducersExperimentalMetrics, false)class PassiveReducersExperimentalMetricsReducer {}The following declarations illustrate alternative activity settings; choose the one your deployment needs:
using Cratis.Chronicle.Reducers;
public record PassiveReducersSwitchableReadModel(int Value);
// Was active, now passive[Reducer(isActive: false)]public class PassiveReducersSwitchableReducer : IReducerFor<PassiveReducersSwitchableReadModel>;import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModel
@ReadModeldata class PassiveReducersSwitchableReadModel(val value: Int = 0)
// Was active, now passive@Reducer(isActive = false)class PassiveReducersSwitchableReducer { fun on(event: PassiveReducersValueRecorded) = PassiveReducersSwitchableReadModel(event.value)}
@EventTypedata class PassiveReducersValueRecorded(val value: Int = 0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
@ReadModelrecord PassiveReducersSwitchableReadModel(int value) { PassiveReducersSwitchableReadModel() { this(0); }}
// Was active, now passive@Reducer(isActive = false)class PassiveReducersSwitchableReducer { public PassiveReducersSwitchableReadModel on(PassiveReducersValueRecorded event) { return new PassiveReducersSwitchableReadModel(event.value()); }}
@EventTyperecord PassiveReducersValueRecorded(int value) {}defmodule PassiveReducersSwitchableValueChanged do use Chronicle.Events.EventType, id: "passive-reducers-switchable-value-changed"
defstruct [:value]end
defmodule PassiveReducersSwitchableReadModel do defstruct value: 0end
# Was active, now passive — only the `active:` option changed.defmodule PassiveReducersSwitchableReducer do use Chronicle.Reducers.Reducer, model: PassiveReducersSwitchableReadModel, active: false
alias PassiveReducersSwitchableValueChanged
@handles PassiveReducersSwitchableValueChanged
@impl true def reduce(%PassiveReducersSwitchableValueChanged{} = event, _current, _context) do %PassiveReducersSwitchableReadModel{value: event.value} endendimport { reducer } from '@cratis/chronicle';
class PassiveReducersSwitchableReadModel { value = 0;}
// Was active, now passive@reducer('', undefined, PassiveReducersSwitchableReadModel, false)class PassiveReducersSwitchableReducer {}Changing isActive updates observer activity, not the model’s read strategy. Adding or removing [Passive] changes sink selection and by-id reconstruction semantics. Treat that as a read-model configuration change; do not assume toggling isActive has the same effect.
Passive reads are not guards
Section titled “Passive reads are not guards”A passive read is fresh when you read it, but it does not note a position in the log and does not guard the append that follows. Another writer can append between your read and your write. To make a rule hold, use a constraint or a decision read. A decision read can fold a reducer that has no filters and no constructor dependencies other than ILogger<T>, or a flat projection. See Decision reads. See Choosing how a rule holds.
Neither passive nor inactive reducers enable stream-id subscription filtering or exactly-once delivery. A passive or inactive reducer can still be read as a decision read under the same admission rules as any reducer. See self-reducer limits for the shared boundaries.