How Maka's Runtime Event Log Defines System State: Event Sourcing Implementation
Maka's Runtime Event Log defines system state by maintaining an append-only sequence of events in a thread-safe CopyOnWriteArrayList and deriving the current state through the computeState() method, which instantiates a fresh RuntimeState and replays each recorded event to reconstruct the system's condition.
Apache Maka provides a distributed runtime core where durable system state emerges from an immutable event log rather than direct mutation. The RuntimeEventLog class, located in src/main/java/org/apache/maka/runtime/RuntimeEventLog.java, captures every lifecycle change and processing milestone as discrete RuntimeEvent objects. By replaying these chronologically ordered events through the computeState() method, the runtime reconstructs a deterministic snapshot of whether the system is active and how many items have been processed.
Event Capture and Storage in RuntimeEventLog
The RuntimeEventLog serves as the append-only journal for all runtime activities. It stores events in a CopyOnWriteArrayList<RuntimeEvent> named events, ensuring thread-safe concurrent writes without locking overhead [lines 13, 18-20].
When a component emits a state change, the record(RuntimeEvent event) method appends it to the log. This operation is thread-safe due to the underlying CopyOnWriteArrayList, which creates a fresh copy of the backing array on each modification, making the events list safe for concurrent recording while maintaining read consistency.
public void record(RuntimeEvent event) {
events.add(event);
}
The getEvents() method returns an unmodifiable view of the event history, preventing external mutation while allowing inspection of the log [lines 25-27].
How computeState() Derives System State from Events
System state is not stored as a static variable but calculated on demand through event sourcing logic. The computeState() method in RuntimeEventLog.java implements this derivation [lines 32-38].
The process follows three steps:
- Instantiation: Creates a new
RuntimeStateobject with default values (active = false,processedCount = 0). - Replay: Iterates through the
eventslist in insertion order. - Mutation: Invokes
event.apply(state)for each entry, allowing the event to mutate the state object.
public RuntimeState computeState() {
RuntimeState state = new RuntimeState();
for (RuntimeEvent event : events) {
state.apply(event);
}
return state;
}
This approach ensures that the state is always consistent with the complete history of recorded events. Since RuntimeState is instantiated fresh during each computation, the method returns an immutable snapshot that reflects the log's contents at that exact moment.
RuntimeState Structure and Mutation Logic
The RuntimeState class, defined in src/main/java/org/apache/maka/runtime/RuntimeState.java, acts as the aggregate root for state mutations. It maintains two key fields tracking system condition [lines 7-8]:
active: A boolean indicating whether the runtime is currently running.processedCount: An integer counter for successfully processed items.
State transitions occur within the apply(RuntimeEvent event) method, which uses instanceof checks to determine the specific mutation required [lines 18-26]:
public void apply(RuntimeEvent event) {
if (event instanceof RuntimeStartedEvent) {
this.active = true;
} else if (event instanceof RuntimeStoppedEvent) {
this.active = false;
} else if (event instanceof ProcessedItemEvent) {
this.processedCount++;
}
}
This design follows the double-dispatch pattern, where the event calls apply on the state, and the state routes to the appropriate handling logic based on the concrete event type.
Event Types That Define State Transitions
Concrete event implementations reside in separate classes under org.apache.maka.runtime. Each extends RuntimeEvent and implements the abstract apply(RuntimeState state) method to delegate mutation back to the state object.
Lifecycle Events: RuntimeStartedEvent and RuntimeStoppedEvent
RuntimeStartedEvent and RuntimeStoppedEvent manage the boolean active flag. When computeState() encounters a RuntimeStartedEvent, it triggers state.apply(this), causing the state to set active = true. Conversely, RuntimeStoppedEvent sets active = false [see RuntimeState.java lines 19-22].
Processing Events: ProcessedItemEvent
ProcessedItemEvent captures discrete work completion. It carries a specific itemId string identifying the processed entity, but its state mutation simply increments the processedCount counter [lines 23-25]. This allows the runtime to track throughput and completion metrics without maintaining a list of individual item IDs in the core state.
Practical Implementation: Recording Events and Computing State
The following example demonstrates the complete workflow of event recording and state derivation using Apache Maka's runtime API:
// Initialize the event log
RuntimeEventLog eventLog = new RuntimeEventLog();
// Record runtime lifecycle and processing events
eventLog.record(new RuntimeStartedEvent(System.currentTimeMillis()));
eventLog.record(new ProcessedItemEvent(System.currentTimeMillis(), "item-42"));
eventLog.record(new ProcessedItemEvent(System.currentTimeMillis(), "item-43"));
eventLog.record(new RuntimeStoppedEvent(System.currentTimeMillis()));
// Derive current state from the event history
RuntimeState state = eventLog.computeState();
// Access derived state properties
System.out.println("Runtime active: " + state.isActive()); // false
System.out.println("Items processed: " + state.getProcessedCount()); // 2
In this sequence, the RuntimeEventLog maintains the chronological record while computeState() reconstructs that the runtime was started, processed two distinct items, and subsequently stopped. The resulting RuntimeState accurately reflects active = false and processedCount = 2 based solely on the event sequence.
Summary
- Append-Only Log:
RuntimeEventLogstores events in a thread-safeCopyOnWriteArrayList, ensuring durable, ordered history. - State Derivation: The
computeState()method reconstructs system condition by instantiatingRuntimeStateand replaying all recorded events sequentially. - Double-Dispatch Pattern: Events implement
apply(RuntimeState)to delegate mutation, whileRuntimeState.apply()usesinstanceofrouting to execute specific state transitions. - Deterministic Snapshots: Each call to
computeState()produces a fresh, immutable snapshot reflecting the exact sequence ofRuntimeStartedEvent,RuntimeStoppedEvent, andProcessedItemEventinstances recorded.
Frequently Asked Questions
How does the Runtime Event Log ensure thread safety when recording events?
The RuntimeEventLog uses a CopyOnWriteArrayList as its backing store for the events field. This concurrent collection creates a new copy of the underlying array on every write operation, allowing multiple threads to safely call record() simultaneously without explicit synchronization while maintaining consistent iteration for computeState() readers.
What happens if an unknown event type is passed to RuntimeState.apply()?
The apply(RuntimeEvent event) method in RuntimeState.java contains an if-else chain checking only for RuntimeStartedEvent, RuntimeStoppedEvent, and ProcessedItemEvent. If an event of an unhandled type is passed, the method performs no mutation and returns silently. This design allows forward compatibility but requires developers to ensure all new event types update the state mutation logic.
Can the system state be reconstructed at a specific point in historical time?
Yes, because computeState() derives state from the immutable event list, you could reconstruct historical state by providing a view of the events list truncated at a specific timestamp or index. However, the base implementation replays the entire list; custom logic would be required to stop replay at a specific event boundary to view past states.
Why does each event's apply() method delegate back to RuntimeState.apply()?
This double-dispatch pattern decouples the event type definition from the state mutation logic. It allows RuntimeState to centralize all mutation rules in a single method using instanceof checks, while concrete event classes (RuntimeStartedEvent, ProcessedItemEvent) remain simple data holders that merely forward the call to the state object with themselves as the argument.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →