From c92197c27652065a90822a5e870e7d723d7f2f0f Mon Sep 17 00:00:00 2001 From: Doksanbir Date: Thu, 3 Sep 2026 13:59:27 +0300 Subject: [PATCH 1/2] feat: add Event-Carried State Transfer pattern (#2434) --- event-carried-state-transfer/README.md | 269 ++++++++++++++++++ .../etc/event-carried-state-transfer.urm.puml | 98 +++++++ event-carried-state-transfer/pom.xml | 70 +++++ .../eventcarriedstatetransfer/App.java | 131 +++++++++ .../CustomerReplica.java | 87 ++++++ .../CustomerService.java | 145 ++++++++++ .../CustomerState.java | 76 +++++ .../CustomerUpdatedEvent.java | 41 +++ .../eventcarriedstatetransfer/EventBus.java | 80 ++++++ .../EventListener.java | 41 +++ .../eventcarriedstatetransfer/Order.java | 37 +++ .../OrderRejectedException.java | 38 +++ .../OrderService.java | 97 +++++++ .../eventcarriedstatetransfer/AppTest.java | 62 ++++ .../CustomerReplicaTest.java | 88 ++++++ .../CustomerServiceTest.java | 103 +++++++ .../CustomerStateTest.java | 67 +++++ .../EventBusTest.java | 75 +++++ .../OrderServiceTest.java | 110 +++++++ pom.xml | 1 + 20 files changed, 1716 insertions(+) create mode 100644 event-carried-state-transfer/README.md create mode 100644 event-carried-state-transfer/etc/event-carried-state-transfer.urm.puml create mode 100644 event-carried-state-transfer/pom.xml create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/App.java create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplica.java create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerService.java create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerState.java create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerUpdatedEvent.java create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventBus.java create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventListener.java create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/Order.java create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderRejectedException.java create mode 100644 event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderService.java create mode 100644 event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/AppTest.java create mode 100644 event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplicaTest.java create mode 100644 event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerServiceTest.java create mode 100644 event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerStateTest.java create mode 100644 event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/EventBusTest.java create mode 100644 event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/OrderServiceTest.java diff --git a/event-carried-state-transfer/README.md b/event-carried-state-transfer/README.md new file mode 100644 index 000000000000..59e1158f926c --- /dev/null +++ b/event-carried-state-transfer/README.md @@ -0,0 +1,269 @@ +--- +title: "Event-Carried State Transfer Pattern in Java: Keeping Services Autonomous with Stateful Events" +shortTitle: Event-Carried State Transfer +description: "Learn the Event-Carried State Transfer (ECST) pattern in Java: publish events that carry the full state of a changed entity so consumers keep their own replica, never call the producer back, and keep working when it is down. Includes a runnable example, diagrams, and trade-offs." +category: Messaging +language: en +tag: + - Data transfer + - Decoupling + - Event-driven + - Messaging + - Microservices +--- + +## Also known as + +* ECST +* Stateful events +* Fat events + +## Intent of Event-Carried State Transfer Design Pattern + +Propagate state changes as events that carry the complete new state of the changed entity, so that consuming services can maintain their own local copy of the data and act on it without querying the producing service. + +## Detailed Explanation of Event-Carried State Transfer Pattern with Real-World Examples + +Real-world example + +> A retailer's customer service is the system of record for names, shipping addresses and credit limits. The order service needs that data for every order. Instead of calling the customer service on each order, the customer service publishes a `CustomerUpdated` event every time a customer changes, and the event contains the customer's full record. The order service keeps its own copy of the customers it has seen and ships orders from that copy. When the customer service goes down for maintenance, orders keep flowing. + +In plain words + +> Do not just tell other services that something changed, send them the whole new state so they never have to ask. + +Martin Fowler says + +> Event-Carried State Transfer ... shows up when you want to update clients of a system in such a way that they don't need to contact the source system in order to do further work. ... The consumer can then process the data in its own way, and doesn't need to contact the source system. + +Sequence diagram + +```mermaid +sequenceDiagram + participant CS as Customer service (producer) + participant Bus as Event bus + participant OS as Order service (consumer) + participant R as Customer replica + + CS->>CS: change address, version 1 -> 2 + CS->>Bus: CustomerUpdatedEvent(full state, version 2) + Bus->>OS: deliver event + OS->>R: apply (version 2 > 1, upsert) + Note over CS: customer service goes offline + OS->>R: find customer C-1 + R-->>OS: state version 2 (address, credit limit) + OS-->>OS: order accepted, ships to the replicated address + Bus->>OS: stale CustomerUpdatedEvent(version 1) + OS->>R: apply (version 1 <= 2, ignored) +``` + +## Programmatic Example of Event-Carried State Transfer Pattern in Java + +The example has a producer, a channel and a consumer. The producer is the customer service, the channel is a tiny in-memory event bus and the consumer is the order service with its local customer replica. + +1. **The state and the event that carries it** + +`CustomerState` is the complete record of a customer, including a version that grows with every change. `CustomerUpdatedEvent` embeds the whole state, which is what distinguishes this pattern from a plain event notification. + +```java +public record CustomerState( + String customerId, String name, String shippingAddress, BigDecimal creditLimit, long version) { + + public CustomerState withShippingAddress(String newAddress) { + return new CustomerState(customerId, name, newAddress, creditLimit, version + 1); + } + + public CustomerState withCreditLimit(BigDecimal newLimit) { + return new CustomerState(customerId, name, shippingAddress, newLimit, version + 1); + } +} + +public record CustomerUpdatedEvent(long eventId, Instant occurredAt, CustomerState state) {} +``` + +2. **The channel** + +`EventBus` is a synchronous publish/subscribe channel keyed by event class. In production this role is played by a message broker. + +```java +public void subscribe(Class eventType, EventListener listener) { + var subscribers = listeners.computeIfAbsent(eventType, key -> new ArrayList<>()); + subscribers.add(listener); + LOGGER.info("Subscriber {} registered for {}", subscribers.size(), eventType.getSimpleName()); +} + +public void publish(Object event) { + var subscribers = listeners.getOrDefault(event.getClass(), List.of()); + if (subscribers.isEmpty()) { + LOGGER.warn("No subscribers for {}", event.getClass().getSimpleName()); + return; + } + for (var listener : subscribers) { + deliver(listener, event); + } +} +``` + +3. **The producer** + +`CustomerService` owns the authoritative data. Every change is stored and then announced with the full new state. `findCustomer` is the direct query a consumer would have to make without the pattern, and it stops working once the service is shut down. + +```java +public CustomerState changeShippingAddress(String customerId, String newAddress) { + LOGGER.info("Customer {} moves to {}", customerId, newAddress); + return store(existing(customerId).withShippingAddress(newAddress)); +} + +private CustomerState store(CustomerState state) { + customers.put(state.customerId(), state); + var event = new CustomerUpdatedEvent(eventSequence.incrementAndGet(), Instant.now(), state); + bus.publish(event); + return state; +} + +public Optional findCustomer(String customerId) { + if (!online) { + throw new IllegalStateException("customer service is offline"); + } + return Optional.ofNullable(customers.get(customerId)); +} +``` + +4. **The consumer's replica** + +`CustomerReplica` is the local copy fed only by events. Applying an event is an upsert guarded by the version, so events that arrive late or twice are ignored. + +```java +public boolean apply(CustomerUpdatedEvent event) { + var incoming = event.state(); + var current = customers.get(incoming.customerId()); + if (current != null && current.version() >= incoming.version()) { + LOGGER.info("Ignoring event {} for {}: version {} is not newer than replica version {}", ...); + return false; + } + customers.put(incoming.customerId(), incoming); + return true; +} +``` + +5. **The consumer** + +`OrderService` subscribes its replica to the bus and afterwards reads only from the replica. It holds no reference to the customer service. + +```java +public OrderService(EventBus bus) { + bus.subscribe(CustomerUpdatedEvent.class, replica::apply); +} + +public Order placeOrder(String customerId, BigDecimal amount) { + var customer = + replica + .find(customerId) + .orElseThrow( + () -> new OrderRejectedException("Unknown customer " + customerId + " in replica")); + if (amount.compareTo(customer.creditLimit()) > 0) { + throw new OrderRejectedException( + "Amount " + amount + " exceeds credit limit " + customer.creditLimit() + " of " + customerId); + } + return new Order( + "ORD-" + orderSequence.incrementAndGet(), customerId, customer.shippingAddress(), amount); +} +``` + +6. **The demo** + +`App` registers a customer and changes the address, takes the customer service offline and places an order from the replica, publishes a stale event that the replica ignores, and finally shows the replica enforcing the credit limit. + +```java +var bus = new EventBus(); +var customerService = new CustomerService(bus); +var orderService = new OrderService(bus); + +customerService.register("C-1", "Alice", "1 Harbour Street, Lisbon", new BigDecimal("500.00")); +customerService.changeShippingAddress("C-1", "42 Ocean Avenue, Porto"); + +customerService.shutdown(); +lookUpDirectly(customerService, "C-1"); // fails, the producer is down +var order = orderService.placeOrder("C-1", new BigDecimal("120.00")); // succeeds from the replica + +bus.publish(new CustomerUpdatedEvent(99, Instant.now(), staleVersionOne)); // ignored +tryToOrder(orderService, "C-1", new BigDecimal("900.00")); // rejected, above the replicated limit +``` + +Program output: + +``` +INFO EventBus -- Subscriber 1 registered for CustomerUpdatedEvent +INFO App -- --- Step 1: every customer change is published with the full customer state --- +INFO CustomerService -- Registering customer C-1 (Alice) +INFO CustomerService -- Publishing event 1 with the full state of C-1 (version 1) +INFO EventBus -- Publishing CustomerUpdatedEvent to 1 subscriber(s) +INFO CustomerReplica -- Replica updated from event 1: C-1 is now at version 1 with address '1 Harbour Street, Lisbon' and limit 500.00 +INFO App -- Order service replica: C-1 version 1 at '1 Harbour Street, Lisbon' with limit 500.00 +INFO CustomerService -- Customer C-1 moves to 42 Ocean Avenue, Porto +INFO CustomerService -- Publishing event 2 with the full state of C-1 (version 2) +INFO EventBus -- Publishing CustomerUpdatedEvent to 1 subscriber(s) +INFO CustomerReplica -- Replica updated from event 2: C-1 is now at version 2 with address '42 Ocean Avenue, Porto' and limit 500.00 +INFO App -- Order service replica: C-1 version 2 at '42 Ocean Avenue, Porto' with limit 500.00 +INFO App -- --- Step 2: the customer service goes offline, orders still flow from the replica --- +WARN CustomerService -- Customer service is going offline +WARN App -- Direct lookup of C-1 failed: customer service is offline +INFO OrderService -- Accepted ORD-1 for C-1 (120.00) shipping to '42 Ocean Avenue, Porto' using replica version 2 +INFO App -- ORD-1 ships to '42 Ocean Avenue, Porto' without asking the customer service +INFO App -- --- Step 3: a stale event arrives late and the replica ignores it --- +INFO EventBus -- Publishing CustomerUpdatedEvent to 1 subscriber(s) +INFO CustomerReplica -- Ignoring event 99 for C-1: version 1 is not newer than replica version 2 +INFO App -- Order service replica: C-1 version 2 at '42 Ocean Avenue, Porto' with limit 500.00 +INFO App -- --- Step 4: the replica is enough to enforce business rules --- +WARN App -- Order rejected: Amount 900.00 exceeds credit limit 500.00 of C-1 +WARN App -- Order rejected: Unknown customer C-2 in replica +``` + +## Class diagram + +See [event-carried-state-transfer.urm.puml](./etc/event-carried-state-transfer.urm.puml) for the PlantUML class diagram. + +## When to Use the Event-Carried State Transfer Pattern in Java + +* Consumers need data owned by another service on every request and a synchronous call would add latency, load, or a hard availability dependency. +* The producer must stay available and responsive regardless of how many consumers depend on its data. +* Consumers can tolerate eventual consistency, reading data that is a few events behind the producer. +* Several services need their own view of the same data, possibly stored in different shapes. + +## Real-World Applications of Event-Carried State Transfer Pattern in Java + +* Kafka topics that carry full entity snapshots, consumed by services that build local materialized views. +* Change data capture pipelines such as Debezium, which stream the complete row state after each database change. +* Product catalogue or customer master data replicated into search, pricing, and fulfilment services in e-commerce platforms. + +## Benefits and Trade-offs of Event-Carried State Transfer Pattern + +Benefits: + +* Consumers are autonomous: they answer from local data and keep working while the producer is down. +* The producer is not queried by consumers, so its load does not grow with the number of consumers. +* Every event is self-contained, which makes consumers simple to write and test. + +Trade-offs: + +* Eventual consistency: a consumer may act on data that is one or more events behind. +* Data is duplicated across services and every consumer has to store what it needs. +* Events are larger than plain notifications, and their schema has to be versioned and evolved carefully. +* Consumers must handle out-of-order and duplicated deliveries, for example with the version check shown here. + +## Related Java Design Patterns + +* [Event-Driven Architecture](../event-driven-architecture): the overall style in which services react to events; ECST is one of the ways events are used in it. +* [Publish-Subscribe](../publish-subscribe): the delivery mechanism the state-carrying events ride on. +* [Event Sourcing](../event-sourcing): stores events as the system of record and rebuilds state by replaying them. +* [Microservices Messaging](../microservices-messaging): asynchronous communication between services, which ECST relies on. +* [Command Query Responsibility Segregation](../command-query-responsibility-segregation): read models fed by ECST events are a common way to build the query side. + +How this pattern differs from its neighbours: an event notification carries only an identifier and forces the consumer to call the producer back for details. Event-Carried State Transfer carries the full state, so the consumer keeps a local replica and never calls back. Event Sourcing keeps the events themselves as the source of truth; ECST only uses events to feed replicas while the producer remains the source of truth. Publish-Subscribe is the channel; ECST is about what the messages on that channel contain. + +## References and Credits + +* [What do you mean by "Event-Driven"? (Martin Fowler)](https://martinfowler.com/articles/201701-event-driven.html) +* [Stateful Event Pattern (Graham Brooks)](https://www.grahambrooks.com/event-driven-architecture/patterns/stateful-event-pattern/) +* [The Event-Carried State Transfer Pattern (itnext)](https://itnext.io/the-event-carried-state-transfer-pattern-aae49715bb7f) +* [Microservices Patterns: With examples in Java](https://amzn.to/3xaZwk0) diff --git a/event-carried-state-transfer/etc/event-carried-state-transfer.urm.puml b/event-carried-state-transfer/etc/event-carried-state-transfer.urm.puml new file mode 100644 index 000000000000..08f2bca14a43 --- /dev/null +++ b/event-carried-state-transfer/etc/event-carried-state-transfer.urm.puml @@ -0,0 +1,98 @@ +@startuml +package com.iluwatar.eventcarriedstatetransfer { + class CustomerState { + - customerId : String + - name : String + - shippingAddress : String + - creditLimit : BigDecimal + - version : long + + CustomerState(customerId : String, name : String, shippingAddress : String, creditLimit : BigDecimal, version : long) + + customerId() : String + + name() : String + + shippingAddress() : String + + creditLimit() : BigDecimal + + version() : long + + withShippingAddress(newAddress : String) : CustomerState + + withCreditLimit(newLimit : BigDecimal) : CustomerState + } + class CustomerUpdatedEvent { + - eventId : long + - occurredAt : Instant + - state : CustomerState + + CustomerUpdatedEvent(eventId : long, occurredAt : Instant, state : CustomerState) + + eventId() : long + + occurredAt() : Instant + + state() : CustomerState + } + interface EventListener { + + onEvent(event : E) : void {abstract} + } + class EventBus { + - listeners : Map, List>> + + EventBus() + + subscribe(eventType : Class, listener : EventListener) : void + + publish(event : Object) : void + } + class CustomerService { + - customers : Map + - bus : EventBus + - eventSequence : AtomicLong + - online : boolean + + CustomerService(bus : EventBus) + + register(customerId : String, name : String, shippingAddress : String, creditLimit : BigDecimal) : CustomerState + + changeShippingAddress(customerId : String, newAddress : String) : CustomerState + + changeCreditLimit(customerId : String, newLimit : BigDecimal) : CustomerState + + findCustomer(customerId : String) : Optional + + shutdown() : void + + isOnline() : boolean + } + class CustomerReplica { + - customers : Map + + CustomerReplica() + + apply(event : CustomerUpdatedEvent) : boolean + + find(customerId : String) : Optional + + size() : int + } + class OrderService { + - replica : CustomerReplica + - orderSequence : AtomicLong + + OrderService(bus : EventBus) + + placeOrder(customerId : String, amount : BigDecimal) : Order + + replica() : CustomerReplica + } + class Order { + - orderId : String + - customerId : String + - shippingAddress : String + - amount : BigDecimal + + Order(orderId : String, customerId : String, shippingAddress : String, amount : BigDecimal) + + orderId() : String + + customerId() : String + + shippingAddress() : String + + amount() : BigDecimal + } + class OrderRejectedException { + + OrderRejectedException(message : String) + } + class App { + + App() + + main(args : String[]) : void + } +} +CustomerUpdatedEvent --> CustomerState +EventBus --> "*" EventListener +CustomerService --> EventBus +CustomerService --> "*" CustomerState +CustomerService ..> CustomerUpdatedEvent : publishes +CustomerReplica --> "*" CustomerState +CustomerReplica ..> CustomerUpdatedEvent : applies +CustomerReplica ..|> EventListener +OrderService --> CustomerReplica +OrderService ..> EventBus : subscribes +OrderService ..> Order : creates +OrderService ..> OrderRejectedException : throws +OrderRejectedException --|> RuntimeException +App ..> EventBus +App ..> CustomerService +App ..> OrderService +@enduml diff --git a/event-carried-state-transfer/pom.xml b/event-carried-state-transfer/pom.xml new file mode 100644 index 000000000000..530176433922 --- /dev/null +++ b/event-carried-state-transfer/pom.xml @@ -0,0 +1,70 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + event-carried-state-transfer + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.eventcarriedstatetransfer.App + + + + + + + + + diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/App.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/App.java new file mode 100644 index 000000000000..98ee9f57711c --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/App.java @@ -0,0 +1,131 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import java.math.BigDecimal; +import java.time.Instant; +import lombok.extern.slf4j.Slf4j; + +/** + * Event-Carried State Transfer (ECST) is an event-driven pattern in which every event carries the + * complete state of the entity that changed. Consumers keep their own local copy of that state and + * serve their requests from it, so they neither call the producer back nor stop working when the + * producer is unavailable. + * + *

The building blocks are a producer ({@link CustomerService}) that publishes {@link + * CustomerUpdatedEvent}s containing the full {@link CustomerState}, a channel ({@link EventBus}), + * and a consumer ({@link OrderService}) that keeps a {@link CustomerReplica} up to date and reads + * only from it. + * + *

The demo registers a customer and changes the address, showing the replica following each + * event. It then takes the customer service offline and places an order anyway, purely from the + * replica. A stale event is published to show that the replica ignores it, and finally an order + * above the replicated credit limit is rejected. + */ +@Slf4j +public class App { + + /** + * Program entry point. + * + * @param args command line arguments, not used + */ + public static void main(String[] args) { + var bus = new EventBus(); + var customerService = new CustomerService(bus); + var orderService = new OrderService(bus); + + LOGGER.info("--- Step 1: every customer change is published with the full customer state ---"); + customerService.register("C-1", "Alice", "1 Harbour Street, Lisbon", new BigDecimal("500.00")); + logReplica(orderService, "C-1"); + customerService.changeShippingAddress("C-1", "42 Ocean Avenue, Porto"); + logReplica(orderService, "C-1"); + + LOGGER.info( + "--- Step 2: the customer service goes offline, orders still flow from the replica ---"); + customerService.shutdown(); + lookUpDirectly(customerService, "C-1"); + var order = orderService.placeOrder("C-1", new BigDecimal("120.00")); + LOGGER.info( + "{} ships to '{}' without asking the customer service", + order.orderId(), + order.shippingAddress()); + + LOGGER.info("--- Step 3: a stale event arrives late and the replica ignores it ---"); + var stale = + new CustomerUpdatedEvent( + 99, + Instant.now(), + new CustomerState( + "C-1", "Alice", "1 Harbour Street, Lisbon", new BigDecimal("500.00"), 1)); + bus.publish(stale); + logReplica(orderService, "C-1"); + + LOGGER.info("--- Step 4: the replica is enough to enforce business rules ---"); + tryToOrder(orderService, "C-1", new BigDecimal("900.00")); + tryToOrder(orderService, "C-2", new BigDecimal("10.00")); + } + + /** + * The call a consumer would have to make without the pattern; it fails while the producer is + * down. + */ + static void lookUpDirectly(CustomerService customerService, String customerId) { + try { + customerService + .findCustomer(customerId) + .ifPresent( + state -> + LOGGER.info( + "Direct lookup of {} answered version {}", customerId, state.version())); + } catch (IllegalStateException e) { + LOGGER.warn("Direct lookup of {} failed: {}", customerId, e.getMessage()); + } + } + + private static void logReplica(OrderService orderService, String customerId) { + orderService + .replica() + .find(customerId) + .ifPresent( + state -> + LOGGER.info( + "Order service replica: {} version {} at '{}' with limit {}", + state.customerId(), + state.version(), + state.shippingAddress(), + state.creditLimit())); + } + + /** Places an order and logs the outcome instead of failing the demo on a rejection. */ + static void tryToOrder(OrderService orderService, String customerId, BigDecimal amount) { + try { + var order = orderService.placeOrder(customerId, amount); + LOGGER.info("Order {} accepted", order.orderId()); + } catch (OrderRejectedException e) { + LOGGER.warn("Order rejected: {}", e.getMessage()); + } + } +} diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplica.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplica.java new file mode 100644 index 000000000000..e6b1ed0c612c --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplica.java @@ -0,0 +1,87 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + +/** + * The consumer's local copy of customer state, fed exclusively by {@link CustomerUpdatedEvent}s. + * + *

Because each event carries the full state, applying one is a simple upsert. The version + * carried by the state guards against events that arrive late or twice: an event is ignored unless + * its version is newer than what the replica already holds. + */ +@Slf4j +public class CustomerReplica { + + private final Map customers = new HashMap<>(); + + /** + * Applies an event to the replica. + * + * @param event the received event + * @return {@code true} if the replica was updated, {@code false} if the event was stale + */ + public boolean apply(CustomerUpdatedEvent event) { + var incoming = event.state(); + var current = customers.get(incoming.customerId()); + if (current != null && current.version() >= incoming.version()) { + LOGGER.info( + "Ignoring event {} for {}: version {} is not newer than replica version {}", + event.eventId(), + incoming.customerId(), + incoming.version(), + current.version()); + return false; + } + customers.put(incoming.customerId(), incoming); + LOGGER.info( + "Replica updated from event {}: {} is now at version {} with address '{}' and limit {}", + event.eventId(), + incoming.customerId(), + incoming.version(), + incoming.shippingAddress(), + incoming.creditLimit()); + return true; + } + + /** + * Reads a customer from the local copy. + * + * @param customerId the identifier of the customer + * @return the replicated state, if any event for the customer has been received + */ + public Optional find(String customerId) { + return Optional.ofNullable(customers.get(customerId)); + } + + /** Number of customers known to the replica. */ + public int size() { + return customers.size(); + } +} diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerService.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerService.java new file mode 100644 index 000000000000..484bd2a8bc4d --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerService.java @@ -0,0 +1,145 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; + +/** + * The producer side of the pattern: the system of record for customers. + * + *

Every change to a customer is applied to the authoritative store and then announced with a + * {@link CustomerUpdatedEvent} that carries the customer's complete new state. Consumers never need + * to query this service to act on the change, which is demonstrated by taking it offline in the + * demo while orders keep flowing. + */ +@Slf4j +public class CustomerService { + + private final Map customers = new LinkedHashMap<>(); + private final EventBus bus; + private final AtomicLong eventSequence = new AtomicLong(); + private boolean online = true; + + /** + * Creates the service. + * + * @param bus the channel on which state events are published + */ + public CustomerService(EventBus bus) { + this.bus = bus; + } + + /** + * Registers a new customer and publishes its initial state. + * + * @param customerId the identifier of the customer + * @param name the customer's name + * @param shippingAddress the shipping address + * @param creditLimit the credit limit + * @return the stored state + */ + public CustomerState register( + String customerId, String name, String shippingAddress, BigDecimal creditLimit) { + var state = new CustomerState(customerId, name, shippingAddress, creditLimit, 1); + LOGGER.info("Registering customer {} ({})", customerId, name); + return store(state); + } + + /** + * Changes the shipping address of a customer and publishes the new state. + * + * @param customerId the identifier of the customer + * @param newAddress the new shipping address + * @return the stored state + */ + public CustomerState changeShippingAddress(String customerId, String newAddress) { + LOGGER.info("Customer {} moves to {}", customerId, newAddress); + return store(existing(customerId).withShippingAddress(newAddress)); + } + + /** + * Changes the credit limit of a customer and publishes the new state. + * + * @param customerId the identifier of the customer + * @param newLimit the new credit limit + * @return the stored state + */ + public CustomerState changeCreditLimit(String customerId, BigDecimal newLimit) { + LOGGER.info("Customer {} gets a credit limit of {}", customerId, newLimit); + return store(existing(customerId).withCreditLimit(newLimit)); + } + + /** + * Looks a customer up directly. This is the call consumers would have to make without the + * pattern, and it fails once the service is offline. + * + * @param customerId the identifier of the customer + * @return the current state, if the customer exists + * @throws IllegalStateException if the service has been shut down + */ + public Optional findCustomer(String customerId) { + if (!online) { + throw new IllegalStateException("customer service is offline"); + } + return Optional.ofNullable(customers.get(customerId)); + } + + /** Simulates an outage: direct queries fail until the service is back. */ + public void shutdown() { + online = false; + LOGGER.warn("Customer service is going offline"); + } + + /** Whether direct queries are currently answered. */ + public boolean isOnline() { + return online; + } + + private CustomerState existing(String customerId) { + var state = customers.get(customerId); + if (state == null) { + throw new IllegalArgumentException("Unknown customer: " + customerId); + } + return state; + } + + private CustomerState store(CustomerState state) { + customers.put(state.customerId(), state); + var event = new CustomerUpdatedEvent(eventSequence.incrementAndGet(), Instant.now(), state); + LOGGER.info( + "Publishing event {} with the full state of {} (version {})", + event.eventId(), + state.customerId(), + state.version()); + bus.publish(event); + return state; + } +} diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerState.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerState.java new file mode 100644 index 000000000000..86ea9ecf318d --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerState.java @@ -0,0 +1,76 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import java.math.BigDecimal; +import java.util.Objects; + +/** + * The complete state of a customer as the customer service knows it. + * + *

Every {@link CustomerUpdatedEvent} carries one of these, so a consumer that receives the event + * has everything it needs to serve its own requests without calling the customer service back. The + * {@code version} grows with every change and lets consumers recognise stale or duplicated events. + * + * @param customerId the identifier of the customer + * @param name the customer's name + * @param shippingAddress where orders for this customer are shipped + * @param creditLimit the maximum order amount the customer may place + * @param version monotonically increasing change counter, starts at 1 + */ +public record CustomerState( + String customerId, String name, String shippingAddress, BigDecimal creditLimit, long version) { + + /** Validates the state. */ + public CustomerState { + Objects.requireNonNull(customerId, "customerId"); + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(shippingAddress, "shippingAddress"); + Objects.requireNonNull(creditLimit, "creditLimit"); + if (version < 1) { + throw new IllegalArgumentException("version must be at least 1"); + } + } + + /** + * Returns a copy with a new shipping address and the next version. + * + * @param newAddress the new shipping address + * @return the updated state + */ + public CustomerState withShippingAddress(String newAddress) { + return new CustomerState(customerId, name, newAddress, creditLimit, version + 1); + } + + /** + * Returns a copy with a new credit limit and the next version. + * + * @param newLimit the new credit limit + * @return the updated state + */ + public CustomerState withCreditLimit(BigDecimal newLimit) { + return new CustomerState(customerId, name, shippingAddress, newLimit, version + 1); + } +} diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerUpdatedEvent.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerUpdatedEvent.java new file mode 100644 index 000000000000..410f8c0256fc --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerUpdatedEvent.java @@ -0,0 +1,41 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import java.time.Instant; + +/** + * Event published whenever a customer changes. + * + *

This is the heart of the pattern: instead of announcing only that a customer changed + * and forcing consumers to call back for the details, the event carries the customer's + * full {@link CustomerState}. Consumers store it locally and stay operational even + * when the customer service is unavailable. + * + * @param eventId sequence number assigned by the producer + * @param occurredAt when the change happened + * @param state the complete customer state after the change + */ +public record CustomerUpdatedEvent(long eventId, Instant occurredAt, CustomerState state) {} diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventBus.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventBus.java new file mode 100644 index 000000000000..7bf876177a4f --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventBus.java @@ -0,0 +1,80 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; + +/** + * Minimal in-memory, synchronous publish/subscribe channel. + * + *

Producers publish events, subscribers register for an event class and receive every event of + * that class in subscription order. In production this role is played by a message broker such as + * Kafka or RabbitMQ; here it is kept synchronous so the pattern stays easy to follow and to test. + */ +@Slf4j +public class EventBus { + + private final Map, List>> listeners = new HashMap<>(); + + /** + * Registers a listener for events of the given class. + * + * @param eventType the class of events to receive + * @param listener the listener to notify + * @param the event type + */ + public void subscribe(Class eventType, EventListener listener) { + var subscribers = listeners.computeIfAbsent(eventType, key -> new ArrayList<>()); + subscribers.add(listener); + LOGGER.info("Subscriber {} registered for {}", subscribers.size(), eventType.getSimpleName()); + } + + /** + * Delivers the event to every listener subscribed to its class. + * + * @param event the event to publish + */ + public void publish(Object event) { + var subscribers = listeners.getOrDefault(event.getClass(), List.of()); + if (subscribers.isEmpty()) { + LOGGER.warn("No subscribers for {}", event.getClass().getSimpleName()); + return; + } + LOGGER.info( + "Publishing {} to {} subscriber(s)", event.getClass().getSimpleName(), subscribers.size()); + for (var listener : subscribers) { + deliver(listener, event); + } + } + + @SuppressWarnings("unchecked") + private static void deliver(EventListener listener, Object event) { + listener.onEvent((E) event); + } +} diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventListener.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventListener.java new file mode 100644 index 000000000000..d715b985434d --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventListener.java @@ -0,0 +1,41 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +/** + * Receives events of one type from the {@link EventBus}. + * + * @param the event type + */ +@FunctionalInterface +public interface EventListener { + + /** + * Handles one event. + * + * @param event the delivered event + */ + void onEvent(E event); +} diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/Order.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/Order.java new file mode 100644 index 000000000000..4b1037995b2f --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/Order.java @@ -0,0 +1,37 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import java.math.BigDecimal; + +/** + * An order accepted by the {@link OrderService}. + * + * @param orderId the generated order identifier + * @param customerId the ordering customer + * @param shippingAddress the address taken from the customer replica at the time of ordering + * @param amount the order amount + */ +public record Order(String orderId, String customerId, String shippingAddress, BigDecimal amount) {} diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderRejectedException.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderRejectedException.java new file mode 100644 index 000000000000..da0ff50be942 --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderRejectedException.java @@ -0,0 +1,38 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +/** Thrown when the {@link OrderService} cannot accept an order based on its replicated data. */ +public class OrderRejectedException extends RuntimeException { + + /** + * Creates the exception. + * + * @param message why the order was rejected + */ + public OrderRejectedException(String message) { + super(message); + } +} diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderService.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderService.java new file mode 100644 index 000000000000..081ab751514a --- /dev/null +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderService.java @@ -0,0 +1,97 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import java.math.BigDecimal; +import java.util.concurrent.atomic.AtomicLong; +import lombok.extern.slf4j.Slf4j; + +/** + * The consumer side of the pattern. + * + *

The order service subscribes its {@link CustomerReplica} to customer events and afterwards + * answers every order using only that replica. It has no reference to the customer service at all, + * so it keeps working while the customer service is down and it never adds load to it. + */ +@Slf4j +public class OrderService { + + private final CustomerReplica replica = new CustomerReplica(); + private final AtomicLong orderSequence = new AtomicLong(); + + /** + * Creates the service and subscribes its replica to customer events. + * + * @param bus the channel that delivers customer events + */ + public OrderService(EventBus bus) { + bus.subscribe(CustomerUpdatedEvent.class, replica::apply); + } + + /** + * Places an order using the replicated customer state. + * + * @param customerId the ordering customer + * @param amount the order amount + * @return the accepted order + * @throws OrderRejectedException if the customer is unknown to the replica or the amount exceeds + * the replicated credit limit + */ + public Order placeOrder(String customerId, BigDecimal amount) { + var customer = + replica + .find(customerId) + .orElseThrow( + () -> new OrderRejectedException("Unknown customer " + customerId + " in replica")); + if (amount.compareTo(customer.creditLimit()) > 0) { + throw new OrderRejectedException( + "Amount " + + amount + + " exceeds credit limit " + + customer.creditLimit() + + " of " + + customerId); + } + var order = + new Order( + "ORD-" + orderSequence.incrementAndGet(), + customerId, + customer.shippingAddress(), + amount); + LOGGER.info( + "Accepted {} for {} ({}) shipping to '{}' using replica version {}", + order.orderId(), + customerId, + amount, + order.shippingAddress(), + customer.version()); + return order; + } + + /** The local copy of customer state this service works from. */ + public CustomerReplica replica() { + return replica; + } +} diff --git a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/AppTest.java b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/AppTest.java new file mode 100644 index 000000000000..989a75eea778 --- /dev/null +++ b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/AppTest.java @@ -0,0 +1,62 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class AppTest { + + @Test + void shouldBeInstantiable() { + assertNotNull(new App(), "App should be instantiable"); + } + + @Test + void shouldLaunchApp() { + assertDoesNotThrow(() -> App.main(new String[] {})); + } + + @Test + void directLookupSucceedsWhileTheCustomerServiceIsOnline() { + var customerService = new CustomerService(new EventBus()); + customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + + assertDoesNotThrow(() -> App.lookUpDirectly(customerService, "C-1")); + } + + @Test + void tryToOrderLogsAcceptedOrders() { + var bus = new EventBus(); + var customerService = new CustomerService(bus); + var orderService = new OrderService(bus); + customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + + assertDoesNotThrow(() -> App.tryToOrder(orderService, "C-1", new BigDecimal("10.00"))); + } +} diff --git a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplicaTest.java b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplicaTest.java new file mode 100644 index 000000000000..1428231e2661 --- /dev/null +++ b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplicaTest.java @@ -0,0 +1,88 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +class CustomerReplicaTest { + + private final CustomerReplica replica = new CustomerReplica(); + + @Test + void appliesTheFirstEventForACustomer() { + assertTrue(replica.apply(event(1, state("Lisbon", 1)))); + + assertEquals(1, replica.size()); + assertEquals("Lisbon", replica.find("C-1").orElseThrow().shippingAddress()); + } + + @Test + void appliesNewerVersions() { + replica.apply(event(1, state("Lisbon", 1))); + + assertTrue(replica.apply(event(2, state("Porto", 2)))); + + assertEquals("Porto", replica.find("C-1").orElseThrow().shippingAddress()); + assertEquals(2, replica.find("C-1").orElseThrow().version()); + } + + @Test + void ignoresOlderVersionsThatArriveLate() { + replica.apply(event(2, state("Porto", 2))); + + assertFalse(replica.apply(event(1, state("Lisbon", 1)))); + + assertEquals("Porto", replica.find("C-1").orElseThrow().shippingAddress()); + } + + @Test + void ignoresDuplicateDeliveries() { + replica.apply(event(1, state("Lisbon", 1))); + + assertFalse(replica.apply(event(1, state("Lisbon", 1)))); + + assertEquals(1, replica.size()); + } + + @Test + void unknownCustomersAreAbsent() { + assertTrue(replica.find("C-9").isEmpty()); + assertEquals(0, replica.size()); + } + + private static CustomerState state(String address, long version) { + return new CustomerState("C-1", "Alice", address, new BigDecimal("500.00"), version); + } + + private static CustomerUpdatedEvent event(long id, CustomerState state) { + return new CustomerUpdatedEvent(id, Instant.EPOCH, state); + } +} diff --git a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerServiceTest.java b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerServiceTest.java new file mode 100644 index 000000000000..79060157aa7f --- /dev/null +++ b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerServiceTest.java @@ -0,0 +1,103 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CustomerServiceTest { + + private final EventBus bus = new EventBus(); + private final List published = new ArrayList<>(); + private final CustomerService service = new CustomerService(bus); + + @BeforeEach + void subscribe() { + bus.subscribe(CustomerUpdatedEvent.class, published::add); + } + + @Test + void registrationPublishesTheFullInitialState() { + var state = service.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + + assertEquals(1, published.size()); + var event = published.get(0); + assertEquals(1, event.eventId()); + assertEquals(state, event.state()); + assertEquals(1, event.state().version()); + assertEquals("Lisbon", event.state().shippingAddress()); + assertEquals(new BigDecimal("500.00"), event.state().creditLimit()); + } + + @Test + void everyChangePublishesANewVersionWithTheWholeState() { + service.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + service.changeShippingAddress("C-1", "Porto"); + service.changeCreditLimit("C-1", new BigDecimal("900.00")); + + assertEquals( + List.of(1L, 2L, 3L), published.stream().map(CustomerUpdatedEvent::eventId).toList()); + var latest = published.get(2).state(); + assertEquals(3, latest.version()); + assertEquals("Porto", latest.shippingAddress()); + assertEquals(new BigDecimal("900.00"), latest.creditLimit()); + assertEquals("Alice", latest.name()); + } + + @Test + void rejectsChangesToUnknownCustomers() { + assertThrows(IllegalArgumentException.class, () -> service.changeShippingAddress("C-9", "x")); + assertThrows( + IllegalArgumentException.class, () -> service.changeCreditLimit("C-9", BigDecimal.TEN)); + assertTrue(published.isEmpty()); + } + + @Test + void answersDirectLookupsWhileOnline() { + service.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + + assertTrue(service.isOnline()); + assertEquals("Alice", service.findCustomer("C-1").orElseThrow().name()); + assertTrue(service.findCustomer("C-9").isEmpty()); + } + + @Test + void refusesDirectLookupsWhenOffline() { + service.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + service.shutdown(); + + assertFalse(service.isOnline()); + var thrown = assertThrows(IllegalStateException.class, () -> service.findCustomer("C-1")); + assertEquals("customer service is offline", thrown.getMessage()); + } +} diff --git a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerStateTest.java b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerStateTest.java new file mode 100644 index 000000000000..7e4773dd8ce2 --- /dev/null +++ b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerStateTest.java @@ -0,0 +1,67 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class CustomerStateTest { + + private final CustomerState initial = + new CustomerState("C-1", "Alice", "Lisbon", new BigDecimal("500.00"), 1); + + @Test + void changingTheAddressBumpsTheVersion() { + var moved = initial.withShippingAddress("Porto"); + + assertEquals("Porto", moved.shippingAddress()); + assertEquals(2, moved.version()); + assertEquals(initial.creditLimit(), moved.creditLimit()); + } + + @Test + void changingTheCreditLimitBumpsTheVersion() { + var richer = initial.withCreditLimit(new BigDecimal("900.00")); + + assertEquals(new BigDecimal("900.00"), richer.creditLimit()); + assertEquals(2, richer.version()); + assertEquals(initial.shippingAddress(), richer.shippingAddress()); + } + + @Test + void rejectsInvalidState() { + assertThrows( + IllegalArgumentException.class, + () -> new CustomerState("C-1", "Alice", "Lisbon", BigDecimal.ONE, 0)); + assertThrows( + NullPointerException.class, + () -> new CustomerState(null, "Alice", "Lisbon", BigDecimal.ONE, 1)); + assertThrows( + NullPointerException.class, () -> new CustomerState("C-1", "Alice", "Lisbon", null, 1)); + } +} diff --git a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/EventBusTest.java b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/EventBusTest.java new file mode 100644 index 000000000000..4d67198c6fae --- /dev/null +++ b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/EventBusTest.java @@ -0,0 +1,75 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class EventBusTest { + + private final EventBus bus = new EventBus(); + + @Test + void deliversEventsToSubscribersOfTheirType() { + var received = new ArrayList(); + bus.subscribe(String.class, received::add); + + bus.publish("hello"); + bus.publish("world"); + + assertEquals(List.of("hello", "world"), received); + } + + @Test + void doesNotDeliverEventsOfOtherTypes() { + var received = new ArrayList(); + bus.subscribe(String.class, received::add); + + bus.publish(42); + + assertTrue(received.isEmpty()); + } + + @Test + void publishingWithoutSubscribersIsHarmless() { + assertDoesNotThrow(() -> bus.publish("nobody listens")); + } + + @Test + void deliversInSubscriptionOrder() { + var order = new ArrayList(); + bus.subscribe(String.class, event -> order.add("first:" + event)); + bus.subscribe(String.class, event -> order.add("second:" + event)); + + bus.publish("e"); + + assertEquals(List.of("first:e", "second:e"), order); + } +} diff --git a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/OrderServiceTest.java b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/OrderServiceTest.java new file mode 100644 index 000000000000..a53eaef0c170 --- /dev/null +++ b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/OrderServiceTest.java @@ -0,0 +1,110 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.eventcarriedstatetransfer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import org.junit.jupiter.api.Test; + +class OrderServiceTest { + + private final EventBus bus = new EventBus(); + private final CustomerService customerService = new CustomerService(bus); + private final OrderService orderService = new OrderService(bus); + + @Test + void placesOrdersFromTheReplicatedState() { + customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + + var order = orderService.placeOrder("C-1", new BigDecimal("120.00")); + + assertEquals("ORD-1", order.orderId()); + assertEquals("C-1", order.customerId()); + assertEquals("Lisbon", order.shippingAddress()); + assertEquals(new BigDecimal("120.00"), order.amount()); + } + + @Test + void usesTheLatestReplicatedAddress() { + customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + customerService.changeShippingAddress("C-1", "Porto"); + + var order = orderService.placeOrder("C-1", new BigDecimal("10.00")); + + assertEquals("Porto", order.shippingAddress()); + } + + @Test + void keepsWorkingWhileTheCustomerServiceIsOffline() { + customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + customerService.shutdown(); + + assertThrows(IllegalStateException.class, () -> customerService.findCustomer("C-1")); + var order = orderService.placeOrder("C-1", new BigDecimal("10.00")); + + assertEquals("Lisbon", order.shippingAddress()); + } + + @Test + void rejectsCustomersUnknownToTheReplica() { + var thrown = + assertThrows( + OrderRejectedException.class, () -> orderService.placeOrder("C-9", BigDecimal.ONE)); + + assertTrue(thrown.getMessage().contains("C-9")); + } + + @Test + void rejectsOrdersAboveTheReplicatedCreditLimit() { + customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + + var thrown = + assertThrows( + OrderRejectedException.class, + () -> orderService.placeOrder("C-1", new BigDecimal("500.01"))); + + assertTrue(thrown.getMessage().contains("exceeds credit limit")); + } + + @Test + void acceptsOrdersExactlyAtTheCreditLimitAndNumbersThemSequentially() { + customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + + orderService.placeOrder("C-1", new BigDecimal("1.00")); + var second = orderService.placeOrder("C-1", new BigDecimal("500.00")); + + assertEquals("ORD-2", second.orderId()); + } + + @Test + void exposesItsReplica() { + customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + + assertEquals(1, orderService.replica().size()); + } +} diff --git a/pom.xml b/pom.xml index a71630d289d3..139cd5f92410 100644 --- a/pom.xml +++ b/pom.xml @@ -260,6 +260,7 @@ rate-limiting-pattern fallback onion-architecture + event-carried-state-transfer From 1cb094e1fb8e563aec3d759a93ed9e413340baa5 Mon Sep 17 00:00:00 2001 From: Doksanbir Date: Mon, 7 Sep 2026 12:09:15 +0300 Subject: [PATCH 2/2] fix: align the event bus contract with the code and extend the demo publish rejects null, exact-class dispatch is documented, the sequence counters are plain longs, and the demo shows a credit limit change flowing through the replica. The class diagram is a rendered PNG. --- event-carried-state-transfer/README.md | 25 +++++++++++++----- .../etc/event-carried-state-transfer.urm.png | Bin 0 -> 113405 bytes .../etc/event-carried-state-transfer.urm.puml | 7 ++--- .../eventcarriedstatetransfer/App.java | 13 +++++++-- .../CustomerReplica.java | 6 +++-- .../CustomerService.java | 13 ++++++--- .../eventcarriedstatetransfer/EventBus.java | 9 +++++++ .../OrderService.java | 12 +++------ .../CustomerServiceTest.java | 11 ++++++++ .../EventBusTest.java | 8 ++++++ .../OrderServiceTest.java | 15 +++++++++++ 11 files changed, 94 insertions(+), 25 deletions(-) create mode 100644 event-carried-state-transfer/etc/event-carried-state-transfer.urm.png diff --git a/event-carried-state-transfer/README.md b/event-carried-state-transfer/README.md index 59e1158f926c..8d1aeac633b3 100644 --- a/event-carried-state-transfer/README.md +++ b/event-carried-state-transfer/README.md @@ -57,6 +57,8 @@ sequenceDiagram OS->>R: apply (version 1 <= 2, ignored) ``` +![Event-Carried State Transfer class diagram](./etc/event-carried-state-transfer.urm.png) + ## Programmatic Example of Event-Carried State Transfer Pattern in Java The example has a producer, a channel and a consumer. The producer is the customer service, the channel is a tiny in-memory event bus and the consumer is the order service with its local customer replica. @@ -116,7 +118,7 @@ public CustomerState changeShippingAddress(String customerId, String newAddress) private CustomerState store(CustomerState state) { customers.put(state.customerId(), state); - var event = new CustomerUpdatedEvent(eventSequence.incrementAndGet(), Instant.now(), state); + var event = new CustomerUpdatedEvent(++eventSequence, Instant.now(), state); bus.publish(event); return state; } @@ -166,13 +168,13 @@ public Order placeOrder(String customerId, BigDecimal amount) { "Amount " + amount + " exceeds credit limit " + customer.creditLimit() + " of " + customerId); } return new Order( - "ORD-" + orderSequence.incrementAndGet(), customerId, customer.shippingAddress(), amount); + "ORD-" + ++orderSequence, customerId, customer.shippingAddress(), amount); } ``` 6. **The demo** -`App` registers a customer and changes the address, takes the customer service offline and places an order from the replica, publishes a stale event that the replica ignores, and finally shows the replica enforcing the credit limit. +`App` registers a customer and changes the address, takes the customer service offline and places an order from the replica, publishes a stale event that the replica ignores, and shows the replica enforcing the credit limit. It then brings the customer service back and raises that limit: the new limit travels inside the event, and the order that was just rejected is accepted from the replica alone, without a single call back to the producer. ```java var bus = new EventBus(); @@ -188,6 +190,10 @@ var order = orderService.placeOrder("C-1", new BigDecimal("120.00")); // succeed bus.publish(new CustomerUpdatedEvent(99, Instant.now(), staleVersionOne)); // ignored tryToOrder(orderService, "C-1", new BigDecimal("900.00")); // rejected, above the replicated limit + +customerService.restart(); +customerService.changeCreditLimit("C-1", new BigDecimal("1500.00")); // the event carries the new limit +tryToOrder(orderService, "C-1", new BigDecimal("900.00")); // accepted now, still only from the replica ``` Program output: @@ -217,12 +223,17 @@ INFO App -- Order service replica: C-1 version 2 at '42 Ocean Avenue, Porto' wit INFO App -- --- Step 4: the replica is enough to enforce business rules --- WARN App -- Order rejected: Amount 900.00 exceeds credit limit 500.00 of C-1 WARN App -- Order rejected: Unknown customer C-2 in replica +INFO App -- --- Step 5: a new credit limit travels in the event and unblocks the rejected order --- +INFO CustomerService -- Customer service is back online +INFO CustomerService -- Customer C-1 gets a credit limit of 1500.00 +INFO CustomerService -- Publishing event 3 with the full state of C-1 (version 3) +INFO EventBus -- Publishing CustomerUpdatedEvent to 1 subscriber(s) +INFO CustomerReplica -- Replica updated from event 3: C-1 is now at version 3 with address '42 Ocean Avenue, Porto' and limit 1500.00 +INFO App -- Order service replica: C-1 version 3 at '42 Ocean Avenue, Porto' with limit 1500.00 +INFO OrderService -- Accepted ORD-2 for C-1 (900.00) shipping to '42 Ocean Avenue, Porto' using replica version 3 +INFO App -- Order ORD-2 accepted ``` -## Class diagram - -See [event-carried-state-transfer.urm.puml](./etc/event-carried-state-transfer.urm.puml) for the PlantUML class diagram. - ## When to Use the Event-Carried State Transfer Pattern in Java * Consumers need data owned by another service on every request and a synchronous call would add latency, load, or a hard availability dependency. diff --git a/event-carried-state-transfer/etc/event-carried-state-transfer.urm.png b/event-carried-state-transfer/etc/event-carried-state-transfer.urm.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcf2b05b3297e0187075c22ae93e2217de0e950 GIT binary patch literal 113405 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 z zFbzR2q4*{!+K6R1feL zw~t@&^M09M?Ue*S&Fj|V<)d={hxFkeWK7Yj&vE4yMMddt0Krm@*f=+Y`w)<&`C{;G z6Ai05#Ws|%1;RR6Hstvo>WwFmEhsOn>Q4x=hb!Lr*KYbwJB%eR3fu;0i0=T45`y(QWLEhs8I}8PXh(LL=TiH zMfG0{L4)_4Xj;z4j8?0pOvxyu43 z`@j1ydlR&lk2PL>WU{lf6#pouxtHlJ{qjYSV^_aV?danW&X1UJV$zp^g^}?a4JUgY zsf^(QI_zMJU&ib?v!{;yx;bd=qhXZpFXkXMlDutVio6yJWrWx!=j+Kb#lS7g*vvH9p(H5)tMkT(et zIkpL%S-%Igo30#&?K(qgVp5w}?fm{rp)_*e%@FzN_j|9-Rdv1(ocEO(bC1gA9F63I zg;eASlW#NDBF9rHdBNr2`nq_qzj#wt9!+M#Dr4?Q4L!^G+lLAa8P4)8xvA9%+n9Mo zX{utl5_Sp-Zyja|0-)fXoy=X{9AQ<9&RXdh>f90v@%okn1NeI%mt}7|8Q|iK!YRde zhPvmT)EUSsN$n3qeGw}hpoqySci$r;8~F>-91V0%1$M;Ja7=8U3fWFaKSSSTJ=Q>y zBX=&Tw?lja`fr{Ld`H=Nsu2!qC0rz$ZQ)^I5*w{aCS#pQMB=ZhvB}koBEj3v54b=i z73;J1EkP0ig*0VI`lv&S!oahaja)hpOkFtS5YNus?CEQjO`y7iROC0(jb5Qe&_P?( z-E*P&8WsJS;r0;*z0;l&L8EUwJIk(K4hA|fhtAKMkUGo+uk7>Ko1?__M0`P{0^2$N z{zc4c_%b$J=t*c?o7#X(X@rBtPnO{7(U^`97l$e8Jxug>8S~Fb{CV%;OWEJn1wQ&X zL8RgXJ&%+6BUicziWGn$CEhN#Kiq!5)+c0Z4Z=~$Rlh>-nBMk2N>Ey*L5V%MF1UBV z+sL|h`l8>z?*0t^by$TXIv8DFS5CVy?l%zhjbSTP^>?`K-(&sxI;&X*W^bPe_dev9 z6Mq|?Is8w8c#}-9ed`0RMP{Wag(RLV&it9c{lC|-M*R_rNT6^HZyxHA4zI3|0*x^o z-~AxjC)Rl1Mcjdh64HpQ0^OetW@(}zR>*SQ>xoHCW*!f76{4=sb(3gZKeer}Pssb6 zM2My$;Kk4P5(!57*Iz&P_)e`F=yZR#Nw99&h8Fh%iFA#1_>>^+zE~~F=Fl^lK^~_> zxaTA5CbS%!C5)Y7ho-or_$&<*9BpM*&3p^@#mLwzLWEdB6)|)mwsaLCYKE!`iZ`o* zmqB_jp0HF5+*o1GI`K}uyRI&6Gd=hdsB*HXPjWaBMhFEh7wt9J3k1H8-NZ0g$P>lH#t-T*QLl3M_Z)gou)=i>a>$-ISF`C*oq7 zm-#^A;%stRq1%y@S9jIksq3>n=Y>e!eQV+m(RF`XH;h@8n5O_KPQMpaVIt9Ne~*}p zJH61X|E%PTaP$Q}sTdWLRdvg)1(Fmf1p6qy?YuPnGiMJkLJu)Ut@y96cn6I@7xyE& zTbz$q`I(Onr{1TLFlK7CQ~6cciZqu&H!UCs35$)dIW5h&lwA+>7V{tME&y`lB39oDo99+6(IG^5V;;5gr1^#HUp31-7q1>(S(K z4<5(v3p1Tr(fQ5;!RQLofw23OZppd0=Fk!i%p?A_29wr0CM?lMDN%1_?pGhX56=h> zj$t%^(ogW>i`Rza`Sjg^%6*aw1HL?8!jOYn%VQXo-e-|cLouIz^6y`ylWB|3G$V`h z?7llX8l2iO_1yj*NMwafdJj2=u14c$7>YhI8lZMP5r6829=)9L|277mhU1q0>wfJH z*kWGL(~S`9YuyK4iP(pQbFze`Ez)90eZ(Gz1|gQ;&I&5I#pf|T_H|b%m)A93bVvC% zh6`fzNCs5b6ndGh>cpmI^sojP(fRDF5ti;4+=3?n{qJE zixd!gmlI9BeM=@E%};!iP-2!DrkstCo!wxmHoqno;yk41F9h$+r*F1@JK!R46YFYS z`kJgmycah?E$>%H&CW zu1+lvZ2J2N2w2d2n1SJ+0}j8So7Ax+567-ghSzK$N98gUfD7>QmYNkawhRvrP!E?^ za$>1?0ow~Qi|Xx{pMW0h1MEmyM{N7Z-a}k4iA<-B#Or%VdCl>tsS(1inxi(5uZMTu z%%+9@_%K&)yKNna>c9fSCh>r%5~p{J_`}Pw9R04N9^^MPG8vMYur#E@1+NOG=s!Cp z-DjWppR}j~EI$uVD|BZMyHldWY>`Ah%i(NWlN(Oz#A`HSva+tAWx4$g_!M*t-Jlh} zb>O_GZXYHQVPm1={_}XP{Aa^sO&CIbvrnS=un9VvgZmdDpb5_mc*n}aQIN>=5*UPA zdd16>O7K9MA_+pHYUQ#l1FlW%f@w!K7g zy>OGzcb{$lX(6)SAy{eJol+;7g`iD+{bXVe^7-~jjOADw{d?~OK6S^3-|r?OZ5H;>>0mAD7)7hT=y4T||>oFZx9k=_yA|YyK!W>9Y zo&cYyNc4~5VsM~V0fj87dv5fvoA*H_U7Sdvv-Y=k7OMwWwIgFuJk@n_y!x~^`^v#> za`z()PsdZ10T%GF@#rvbv4Sr{8o>>yf`0ULu20hQ^MzPH--&fG0>};07f55*8LRe8 zTdB)p%{QVlhd;dd6iBVd4=z5X{=`bv?6=jMY@1nPz#RYJ^q8R;W_{dzhKfk_cnJ6xJJ=o4{(-y`eh9!N1b|X5_*72);RgBN1bh;+lU*b~m%AVJaDWdE(5F~y+)YSrZJ5ApJ%O{&Jw zbzY^+vq;j2)k|i-ngkL-}4UWGa?1iS}kmR z&VToxm2ecSy4rH)#o}&rA@_frh!6hbhiFxy8eUZpYXt%$zk2Ok}uYQvCiXLK$zn3oZu-Z$`!i%jbPI$%eE>y0qVhS#7B|jXjBPX zn+jkpJ`*s1Pd;$0^G+6!c6Awr;v- zZud)FScyW0bOuQMqaeHD9R(ePy$)0eKVa&bppR4?@in@H35)U0M3f+-aHs_G+AW(! zNv*{v&iboh-XL5D(KU#S7QMsat_X`r2F#vX7g74uONB6&HJ3+b&A9=0B6JBQ7W(^y z_VH?Itv!t7>}M73_xYJi8yaVt!s$nPeyS8ehpl8--w0ahHFzZL<#HPtbl0@R{dTWE zUR!0|VIBglNJD)`&?UZ(`jAs#_09aAcvQJC_4@gCnZ#u@ET`XH$t!cdAHN(Us5Mve z^r8dCNiukn-G9K|Sw}vKck76A;AQK;WnV-}epI9n4viL1+| zY<~QZreO*A)c*pzA;z0;ZtdPovY8^XHhM>R_1cWiN!Vi76UM>1$q+S`O4gcGGwczI^XCZ*QF9Qtsd`pQS$X_0Urwgg5?X#<3pfiv7X|X%7mP`CpZrD6YvmbGO}fwmq!6rn^&h~)xViYt)L%CJoy6IUzN#tvY zjb(a3dS4`jJHhkWINFdtrON=zMe zXpSkt4Wf>^FB;RwdnCn57T%^X!3`-jKXJ0AGo4vyvI%S8UhtBM*qu+|8=}f*U5l>L z%`;$x(epQA#FwFX%487{mMx46=WzoJ_FKEFU%vNt`ZrmJ zE-0n__h=0`SIdn$=pcqNRCI^Caeh3?gAAOvlX%XZPI|1t>Sr!aT!0APbPuGm_|81d z5*OM3_Gmd_fhOLsH3aF1l_J-NZ%U>lncY1nS)d`drVrB$5if4?4op4dd#VW)X?>Re zT&@0o*W(RwzU+Ypk$C-1sLc?-)-Hp|GTjjbezY8&m0-MzX}LX$HRM;do@n zF{Tzy(wUfU9kN<)_;=-fU9^~#6daAfjSqoPIx`$Q(I7UA{h$=|Rv>8mCWQ%eNGUf< zx7VErE);ay5xxU^67c)&Rc7wSt=$UpUGVrti|c$=u6pR?+e7qVyEKZcw0DY)4bk6- zwwezHR8z~hMU-xIHjZ0iI5uWuqj>6#UviXPG{t((pzD`=48vq{;%}|$M;-MP_~1=L zGdkfED~39w7gSZz=QOhXJfiTqW|~a;v5K-ub-BflGMyLYM&q4>SS zfr;(5P7}nDjkJTzbOWu_myk;gxgNSXhC@yGw3yE2h@z=ig78Odk7kxK)rC^9tBdT* z2+rtwQ&?G&sfx*Iw`Zzz%Hj5d%TT8Rgr*i+^ile7Q5x;{-e22t8OoYYK}wK(PEmVi z1=DutnK}K~+b{330i_MUb8q)9=(W#r@DR*;jh8$5ek5tr9*Pb5<1EPr-fOhqew z)UdhCRy-1h&DtRAYVeyK^a^oWAol}&nrcPc+VoFAX{t$H!&KMmyDZPUJN+-;O3bF# zMMttxwC=;98?dt(YJTu8xK)Fxw7 zKH@`E!~MT@KwpbpWjDK`iW`|RkvWx>3lh$Me1(-!eA1fyWdzhzx+sP)k)EiwP$HNz z)d<0lhDKplp=1e7C@`q9d$b!Yw*2n1RD`qNCfImY;$vho%KT&CS%%YR?pBLt6DQOo zfu5|e-a-6JM8z)A6*wbJtif_FrOUnGL&i`wD!h`~gLRw~#^1pHkm0XYc{AbAlFxfw zo=YJ?ZaIx+5W1YZKC-!~4y1;R2v9qA$Yvh>>xpN}QyW;p*II2#QH>}0hApj1;EF!5 ze4U6>u_7N?s+2nK?LO!Gs~a+WzAVyGI4d*`4JLb&pTXo^+}-OO;fLJRV-hhDN&5lN z_^xmF(>!};WF}VI0GJ6I5F;bkKO$FRFkD!%+-7TN48!dR;U_0jFky+>MKcSCIWNW@ z@6DRI&!@b$%YXU?t3SMgEgSJiM`s+L+bUp@P_@ftn)WIc7Gm$~L)ny-fD+o83ayj) z>S;){Tgk*vQuB{PoU=ue7&4ns@?fc@<(WUt(Y!wp3@%GSWg9QAHr> zDPcZ{sKS&8D?QYk=Bz-h&nGS_jb&RI4WBdS(4U5|SwP~^gl@*>3f#$VZXsQRANm0q z@5t9g?qZQsh10^%f7V;ZU<{Q4;K5{0Qa(}!0w8C>yPRx9YsC&ks^}8bKYwNVGH`#f z)0bkDd9%PQyL@|}!M>{wPtLSxV`v_*WR~^Yv<;8hRDpcjx1RCX#o8E_sL9%}*H&&x zCGP~c8c^%4(}k6~JoaN`fiMHu>@XFSG=*IiXv>nq*Xd?-pI0jgFY)I{=0k~20Cfs3 zO-z}AVq9e`8&OLx1y;C?{4{$Lz+b@OLgo{Hi}G{F>xoTZH)2h26Gp#MS9JJr;AP6#2r=yE}LDUzA#q{()y~$U23VM0S|n zwJwf#8gfGub5YXA&CNoDm1==Ka`Yuamlo#C!XPqWOncXA2NigUNz zG!3X&sM1BIi~hACLyd}dX$;(v-!&x1vtL0EQ8>%-{aA;bD6Ca#DKr(>bzuR%52i9- z)_o(1!_Z+cpo}h4n3~py@YD@1k=b}Yus##rAwfa8#&Ft+Lrxwzv;0TGAS81E`$?7AA8i-AKwFOE=T#Q3 z*Wv^N{kG1ii}qGb)u51DlV8TCc82`a*xtqW&;)duP@9wYQ)atPUPr_$EnYiMT;k}0 z-a3KLB``nv@`k|BZ4lv#RScYahz(&5Ppwflq5YcmeyV}oUe~d==mux|ZQt$Z>G8be z>`eGw?T4S$sPA&p=vTG&u&BNwNZ$Zuzu)wW78AMAmCd7+vF7aB2s>oe{296;5IRp{C3&6 zCzSL?go1QE-zRAre1V)Wj(2eEr3Xa{0<%o=+F<|AxOuqil)fvDciY?_$QA zHTof(-d=r{>p_k~mVCn$MaLok23}rO1~SUB+g}K7(YD1kY~yuQYNu3z$CRh6j~5+ z+T!L(dG);_kEr?ILpNP*!NqFxx;bX%^!V#m5WFT;=b1yr z;BVw-8`=S2!%zWCtGB1G*3V>6;`uCz%2ds!g5XmUjIx|`qN7S4L+IS&O{ zKgI;Zu5OHf(dND@sXjK`c)Kv(Uhfo9g@pcn7VNyDed9awM3JvT2a)mU9~JuTec}3N z5#SAH{$&0`jYEVXuha zY9$6Et7~r|8JhOZB&s#~k5LlOn<6lgs8_y$BnZg~vP9ifAKdkZqTV`i7B;oI)4Fe<+*af59^sU`dN0_pr)GA;m-K#fc>gWVXLXM1}AksJ1O< zy=~@G=hHK$6A_i(Q`V4jF)kqhS&=d|yW92$Jb6+dpFapfYH;b@JR;{zqxOf@qxumM zI7M`mv8BExID$lZzG(-V+eRNmPhF(zO+<{iR+z?HyH|hOu;r}=+K`nKp1EjIIXb_k z#(s6{x{b}^LOc6y#m~B{)kE7QOtA?aPP3VY3 zZHv30BOP@}I5?-%kyjBF=*R#X#5O{*uQhrz)(mJ!)cOPy> zMYxj#D>YyGXT>1$^oh6JbEL06LQ-uiFjC>ETUaQGd(jML?cVxu`w34&iL<*XIsXD} zRa+)75UHRCNp$!|CBP0a4(_*49a%Un`Zzo2cn64K z9B8+Pze6i%-Q;ylc67j3kkg#aK|(r6 zp$uDZ?E~~pQ}Cy5d`$uUIGsG;?rtG?eKDT3(-fOn*?oqM;P+YbN1T38=phlQ6xh@F zvN6RykGP*^-1ur0G143zcNmG<26Q7S?`93t(JxUb;h#_ zfkY7@k{Gcgr{14#5M&uQjRHxZzH9EE^T6ZFV)m8CC*y70JmmWjC4_l~YJA-QCI%+e zdVMgndFtGEhCcK0tc&R1Fl+0C)G_eN5DJYbGTbiNwgI6S66rFNjwST0(^sq>N`0qK zWG?xdzvgOM7_e%H{fke>PJ(6_%x9h5WsczrNq~dw$o-oqJMmocE6FZ-(iNOzQ~fex zQIwB8_xDth_c96p#gHTB2Q2qjwV!Reej;?ZbH6F_C2%_I^$5CT$=#RmbLCRM+d2v| zm?64bF51@0r4s$eEMYkx=Pv*}BF~p6bsK1h8aghupSEGRw0CC5=RuUS6)Zs!aVM~b zJh`*XFoq=tY~s7;4Sio?Ti$dC@iYX~Ekh$oIrjiPo=t8y>Qds+FYhiKk1n<45-uBru!DqSIx@b|~ zAvjkz(ON19bp_5n&)-MMf>cs7oZVSMAFyUY3&OEq1%CSSrehzIShyBiSEDGps0oz? zeYJg`G%#q$`5*phU!t-fK+eVi+YM_GAn1S#i`7bYU2%^(aCn6!79`!~^11E4L|SEh zAH`l=z+v{+}rXiN1JK3d}E1!rWc~ zdbn_17~!TQsWPMdM-w$Oh);GMp##>YGJU&GhrcD`B2mYW;9HfJijl3G$}Y-Nr>b2j zs>@p_J7+9dofHmliwq#CA~=BQCNc<{W|e^3AGHhrjhXHrynb~+t1+Qr!*a*Cgy!08 z89W>UE8nSpgqRG~bFK4-Q^eN)M8J%2%|O-5uaBw{d8Oml?dxV2qLzWel6?C)fpO#! z1V00YBB_plt0h3CdQ{NzM{qO#Cu%Xml{ z=EXWAixb1{;?&cgq9Ck03=fsu{MZW|FLSnK_bjMbr@|1Gi1fRlWOmUx(!zyhCr(cx z_b>09b+evNM<6TM32RZ=#py<9gM}XZxxjkkW-t|+M?kvUhjDwg<3F!;6i)A=Lb3N! z;EeG_^*6R*+pXd8Aj9gTp@E(sjx7VmAYvqKYC0*ygT;nBS>?`*q3G>xutFBEf{#2gk><#{9LwY>OzK`s+?(CbXlf=yMwUAAc zJ&-Nb_SJ#M)xc9{vYYaX^B)_)g=}|i60?dbVQ?PMIPw%h4Pduo(L*ZxG@R@L96f_* zhj=&3T0pa7H(vag8q!sYJs&#Q|85_IBRmOfD_n-Ktf^xE<|fUR%y+Lo=ucN^ ziVP1giV=d#wxo|%DzffR-`?H*ZGI`&U$5=2K%xG~7R6!DdNp#8R5%xZ^Y2T4|;i z-zpCefexZPC{+Sz2!jD@vIBqEI@g6RBvge_D*8}MU&?w7GK>o3r6ad4Ly^Hc!#yK) zaw*$u#dwe1Sg+En2wIGaZ%+%2DN740uM{%J_5IteQI9eV-Dg@+sRwy*gIT0)(mT9* z&>WH_^|ikiQz9TC@9mD)3}$Xq_{)2?!CJc(8SfD0t~OgS2u3k}!O=~R6}>hLqO-fJ zJ68sE%WXV!qfYIDSwJ1=D%Gm%{lbG7ub zwY$9&@fYlh_9KyoPRj-F88)GKE+{2*^9AX;ZAN#1qPNeKyJBo7-%pL74YfZd1;bG!V{ugDZ2nE^Gs zJp76gq|dN=HiI!cqh%?%uzeuJiX;-2ecmcn3~5jiUDWbhJO;6BD7BHMc#^&f{?kzfwTSzT;A3=OtkS}5C?AxLk{l|4Vex_EDUS84JqJh#tPnJ3%*9=hq!^G^FykT)* z#AT4HYwbs*-`hTctkkFfJEnW#jP)Lf+(NhPl+m6K)<7U?;A{d(0%F+hEr4cLu%pe| z4e>uoPd?HP1v^<|F6H+2J}iMypY&gh#>Ji9zEoNU(Pn6c`LrC#?^iirx{Im%np=~; zgb@9yzEcjXF7%m)J*SuWCwNL9JIzf#OM$}e?6u!ly&sU9UY}y6Nbdm?#JHl*Aytgh zBPmAxb!Eiht5TQn zwZ8S6nV#<(4#Hy{YFok*@Lh$8q!CSJ6t`cV;Kf}&T^@Vi*Y8T6r+YR)5^rWAUAJFWr!Xj+Sp59Xv* zS{*N%Ej2AmIuskt#PzJ7;qmp#<-W;Vu*b(=+{Ehhktw_F6R^yG`A5e4VdO~-OUe(G z`*bqgLcjD2h4Z0QJYTi`LQhg@1?MwuSZ28kjjx|}>J!X;%8wc&1Yi^z!&V0>Qy8nG z18!Xj`h5({P?e@HlEW0rBp-G$@^%QIpM43bf5x#Sea4rl+dN+lseVbAKK-MW_I9t} zh&jItdb_0D581>`7%w(;D}!WNNc^eTKQ_xI$J2-OPwZCA4J@7jGqnFPG)l$sU#{w; zjgDjxN20A}5>8F&w)qS%d^E!y&L6|BaxG!IJ)&Fs**cZ{Azc3}?| zc?8SMr$@Vcyl&hvcK`)zdyvcUG20vS4>(I2YWCE=ENA`d0?#&b4a_@ z*XT)?O5ynDvN&ieZy%+=(v&1A!^j4ZdR^FO*~G(v9{{e+!W2vJ;H4jdG+v>G}C14?toq2=@e?oI&*ChS_8DBnm@_wU=drg^;!81Qu z5FqfMWcJN-S!!@{RDK03oWH3eV1%pS#CtG0@;xe}jvX%l8av>mCy-Kqj9^-?@ir5^4fJaw`crM?oT8SQ`M3hLT;WsHodQCHG(s`7GMD=Qg3pGi1m&IaRTs5s}g`P0I<88K$#eH92>k^&}EHRljtgbWtL z-&N8U&PWZ#zOodXXu@f8h;O;*qL0$*oqnFg$0AW|+)u9p)yN z3xPep*WltvT@JLVeay;V<%^3HLtLTrnW)Chve&KA((O$LKPhZ+LLr4-miAR4t?#aX z=B&0)wP+>P=FH@^rkW%c0%|MIHc-d2)s@BsQ-7E}op%WRTgUdF7`*oQFLeK84+^TI z!nM02>m3v7FKC0sMv$;G-qK#ifEFYRx`aJzWlOq{BLxfRFe#hMmh*NdYk1|=d)rgb z>B22P=-two|5fVTit{X#mpaE?-Pwq5ZaPDJl&2o7{2zB` zcXtv_5xmP6(aned>tx&Xf{1Of{y0m5Br^X4E{`u4r=zbz2Dd2%&pKNA|)gGl2J__?4>wazV6DE3LOzuY( zpG$UoIK&p=cP$xVF})2e{K{-Db}OvUN0sv|U0iFF-E6n><@eRt=JzY>7^zT7JSkiD zQGhk=wN6o@ENfmbY9x3_HYukpTkE|qBsGgmu;ee;2i`c-c( z%^OiU#;Vg9ztw{3`M@pV^92N9K^&~VP0;-`pvuv$! zWfm{iKL_)2IE5%#u6WbO6A6R-UF|Ipe2Hfx)aLHu=0Fjw>=8%np6XfWlReg^`pVgJ zf;l*jECQQO7S_&0-c)d1Lw1E`oRKWWDrZsJcmS9C41Fs^0$g~s7lDh=SN$fvS#1c1 zdyM;ox};+6$Ns+x5r7!wE1#v_oV7`kv7?9CaaJB=h(B_UIL+~J?*7P*#?)07=r7fytM39e!ZhbmsvH~Q&=8Qd|hfW z3lZ952wppQ^FB+5ABl?L7VTKVU*(IDjys8ZJZJ^{-$E z7DNiLkMYR8z`qSur%5f>B#%%5OJ-l>iY#HkE%X9?Txps=N4PJN1(6bl4IKTB0^>GV z@SnDknZGl9Z;(L0GOp5c_0RaECLn?ilgO~a`U!QyqE!ZNM&fi91p^hljlFjXO?x-9 zB<&m+j+HoL?y%?=qIEsV-&XUNXi~Rl8HWD-q55M@K&2&9M%?>MNmRPF;4L_NMcjH< zuH25fb0m7j8bPmb+^NyduF^19c3_yDHA@1%Ar%3* zq#DBH1ZRB|g4!MIl8y7=f{zI99n~nx63yT&$nXHqyX~Ioi=eLKE|FX z;36Axla^eU;6L%&;h7fEi#!uIi@8n%3n)iQYvT@iMWBmo-SbC^Ev&b|m}N=ueGfoT z1nk8zsF-71{^EY{MmLgdCdV^&WuwD<6iN?atfYlq>2#Z(Y{-+0kF_#X=((%P{z})9 z1f?9`y3tf&%3Ftj{H|s4o$ntq@J(f+3;BgNchdsKXj-Q1sH~Fv7aW*fu@G|edD0J{s+P! z9voyT>*1W-uO8BlD#POx?8ps9Ve=hd^gW1|9epWsT6}jm>`9XG-kbm81ymEJRHm-y zMRq6NdvPPxgL?*jqN}g6T}r#ujqFnbcuNHtr3MRiVc9Y%FJ(t4N#5!oR@PvPV_$ra z!x|2zg|3Mju!$RA8RWycxaCE?-CvujQWljXIY?$++Y8tc1chIuU6FP&SbHf6%cu09BBjJ7-E(NxTbW&YYuW2qH$$T!uHegc~v^ z4LrVa*(%}Fi5#0U$A$Z1*z5HUPs>=Dhz(Ess%wDVmx~2Ton_XagGgD_gZKlWn+kLB z{Fi~Wp4aGSb(mMIc^4P;CwiDW`gFpAkGBW3?>#6?>l!vy_WCu48(oU1Hc`njA-R*X zk!9tvF#+Y-|J?=RdMK+2| zKr8^p=FGLJQ#$oFQR{Mjz%IDy%_pnQWlD~2s553fJe*c9MNjNrJ{C|;?cA|i&ti2B ziNO?7i#odAb@tkEo_{OmJ>v|^+;OE*OkSIE<4bAqTfq_APTg(gLS<7@q3^`qY|+rW zes_vml~*{(w|QeyO&K1tZ!K)I1_vcrq6DH6}~P`gLE(Ze}n`+?J=cqwYmd#D1XYHmN5 z(_ksYd{P^J2A*t$uj72#1tOZ5C{5I#k%^5HE_#@vzA4hPF_JDbW5?^z&$Qc_NxQb9 z$=;5Szn}d`%D_N~-OT8(^)4PcFNc$eXoD^8o!AfNE2BWkwaf7f$w4gPK9`jZtZ8A*oZ+%FOYHF3)rD=jHik zzU9kSoqK0{4c@7V;GJsh;5#%T{N6?P$TwBQfI;n*(>EDSs->m_=cgS6T(QgMq^jD% zkShy4lkq^Qf~QXJAcL!i)bS^X7B8r{3O!^=w`sA`^_wlDl~?*u+J~Y`_x?9KnqohM z6?{m?WZt%Sz5!8n3JLtM8r}MILp#N6nUR-D$i&bc)z&MSK)y!edvEbXVnA|2vsCkr z=a+x%z&M8>i1Kj!_M4ksS5F^NGtUylhd{NE45(hQ>M95=$< z$4@`=4e?Dr@pGL2Hp~pEexE@UhL-=g-Y+kVC0tt8_I1x6rsc5Pue1kxk&>Tq^>4^% zTlKAaIq63g{j1w8u~mVh?Iuki)H6)|Ev_J&sfl{S)51smP@!gYXDTW$f0Aj+*>1li zb4uIXp1|Z&pH(G{R&h{|357FoEGrZ_q?zqk3nR~nj3q&TDiGQ_mm086?DG7vWjs%& zQiGz~Rk#w9MLN5HrCd7$-E;FK$>C14jM~uR;m)>E>}=&}>t}MBijfY|m9v;E^FgR4 zYbCb!mdgIbx7PR@<1yk#90RlZo?4DVlSgi_q3y7!NyN@{VMw_ne_7-%ulenV#6Tk! z6)yN;GT;2zQ97a&6HVScd| zwiJTZJva8N9P9E(DpzkqCQ-18C5~Z4>TgeW%;lKV6_QuPj=EYLBCJ|CUKh7=pU4Bn z4;Mz`kLN)$PxAE#j_xcm(BqAU7@zh5=42Bh805}6XA;(gd%?$RP?n)i7^fx>bs?G$NB;7Z~cf~dO z$W6uV3QE~NX9yM7e|iZw@g<07=3^G8%s5jYSFO%KaMZ2|@07PO4>t{qjzSi6r#CAH z@*xGMmbR?MJtMcKRl*{0_H)h>C4N`+2^zj(Gc(8g#F;13-C5QcXZAq(y1f?uRw1JPvCr7qIg>ZX) zG+wWL%>lt^;Z;@XTR!Y^kZF^ zRpjA6Q2u0|Xi7e@#VSDl9Krw6t$w|3!63LvH)g2+zpymjEzB8fKPG6QKZ^^NGW)eh zs_F;ZL9<`Ul85)dC^kKb099rrsj5K|8?l(B(`=fuc`4Nk+X=@wS1D*l@^eyA-a4P-lF~=6E?;-Yd)8n@3F@#{l1YE{j6A5Xp}_?$eN18 zJz77YZ-`|eWKgdj!74raiXpgHbyx7tZ2dH`;=i>9z=6i-1kP#n{W0GxahXxc9gr(a z!fCaX9~;muk3S4KfK1%gehyEbjea2zo{U@y;N*i^;^}AZb4a>5AQ&}Vua~E=KEQzK zOy|Cb6Z0yyJJi|%0v72zZh3`-sGRMhu^x=OYlrNTT(~`c-rxOXEP|C=#jWaxt^*=< zY5cDR{a(h*FV;D>UniO&B53hp^P{+rjjnoe31_l1`qm*{N*6VdzQDoFSs}4Q#53fb9AImYy><4bNWNtqIo<^|Q%bXHjO|*$ z(v{TFA@xBkCs9O?ehGW8eDiji9mqSc)&2m>yh)k-P^{s)n96F{&m4)*XCm*4G zaLz_20FGR?tW~Lwh$4q2F4oNxM9%zr>syrm_x1!SCWWHTqM~a|we<}_ z1RU&LlseMKL8L-511D@jWvxWR2b?$4S5buusE6)zED<~R(ziDaObe>s7Yk)nM#Z56 zU{`cteu;$=QoQu%LHbMKX`;A(-5_vv$H6JD+xB-eKi9_uY(@e4M5kP4lc0V5#dXzi z0Ii36lKs%ZviNX!OsZ5fzxNYX)FzNE;Y|%dQWU8pvxS@(FL|%15Hq~1-NiBV=K#oj zE0kJoN&Vu&vJsm0X#((J+Ex|z%=W;goOf} zetZ9hN7W;iJO*#+-1P2sGr0$zI@~QUkP(Kz$7krVT!HT~Azf=gDBjfmX<=ewbfime zijw^OvY2WjtqO2hqxe?3K8w_#J=)X@4ilET*h09@1m7iJ@{p zNzz+Yc7{07i6DdlU{L42A?6hN%?!?;8);z0^4@nK$Wo9Zb;CM+NO@!#`B|a}d|~-p zv-y;@vk}2J$cJF>`%f0v_ohC>uoMee-D7H)gddm`kxD$xB^F{9ZGGD`)A#v6B@AIw(! zb}NY90W?KmWw3rLmj4c@NW3ZbqC65ugr3L{`&*X&)7DtJ)#6_uD)YXzmcxca*2Bh>bv_l8bpY~&)zM&U-k{e=}`+Ze43Sn+j`mtq6$k?rW8N?G6qMSv|q z2*Y@Y)pup3jSR3~7?|`2!tOm-Cjb9P&3wcEe?%T_^ms3DpFD zZWYlEZGG6`XXrqdr*rfdyPb;2>~UvpPe0hqt(8~tugX?2n{9xRt$bB0x{ zuYW)p0};bp<>B%Mr!?SrKPf|e?Hg`Ge8+xxcY08l(I0>*MsDph9@g@U&gUOseUP4op##(BvUP!EoKQrmgSN}2xFaYiwDGk#z>O1lu}hU zZsA)w@}$f4i&;XD z!arQ=3lSoko&-b3ucoKxcy$iQRM+H`<@q{}b@BYHiqV2+GS(ZAfc5kv;ZDp|?-|u9 zhXc-8^E>bBrwf=AxHpo0ju4D&qSm9_1z=vh#aCo82!3w5a)?Vd=J3(E+eMEpg&Zw* zMf&r@A{D9xgiFVMzqU#Ls?@xin&El><^)x;%$jezda4-Usi9e7u{PwX5v&bFsA96U zRGH#+VuZr6qw-*-ldGo&JqxrEMwSA5RY6q}ILfTrZSfjGx&fJFr&U3XeEpprybev$ z09UEvo1NfC?yi0Olm)xbz@Ts-NsgEWloZFK$dX9ebsVCJfAoLfUH%6{{BLBdSKxDa z@oj1eHgg3Lx-ZOM2w$A=_Qn8A57E&)FWs2PsO^-6c?LGfw11>4w-?e?c%&!ZgXaA{ z;mZOr0&{#_c4;n-P~v(u>*loPQ8Pn?onA?7+6CQzRj};^uCpD?ZdX26>JlS1*T*pb zbJ9*Xp2ZGY#vaJwRUXEAIwEIdy*!K^LR@!!rQ->oxC@ZgjZEw_Do$BYbPUhTpdb>T zjEBp)u;PknNXpA#MiGHaivn#|-N6f3<3ZUXgL=OS&zXCC&q2T2>AO_hj)iHMF*=c5 z8q8YD#6iOtHm9Vw6#F$|=9A&iZemERTuX5=`1C2;H|3_RWgjoSz%~lTF->8*t|pl> zd$1}-b3UaWh?)A%*Dd(an=7<8~H^1kJfaXU8lqPB5L8%izA4Z2s_{m@7}uIPDOK3)$`$sCe{|~7sBPR9E!|#ol$I(w*uxE$y zJbn{mdil1@_8Wpk!SQ-hbK1>TDick2z;XTT;Xx_fx(r*5O|sdB?fZ#Y$E@kcMqz`JD;TLLA=u4kjE)! z&2%^~Cncy`<9NM&pPx5@&LcHZ6~iYF>&eWmkkr6F6bi%{z+Sok{O=ssHRC7aM#7O| z@zC?-o&29HL5vt1bcfvF*t1j@4_;K>R4+Isgr$YU$jV>|_hXin>7ZHI2V1?7{t=dR z7-p2C9s1oXbw{8=(k}oUj3QRgG)NEDZTT&1g{$0VN92xYj98nTxkVzQcvPsVg&!e~ zay2GJ8L4vG5AYtJe!hqP6q4es#BBr%9#x||@5%$gkhgIP9Oc5dZ?O43eu1>|MfEKd zP#3~LL}3Pv(*Fui<@9Sw^XLb@=`aRz2DUY+A|M9fDLpde?qENiy#)@!d*HQYfeYd# z{iy(Sj`!^qO20=sz{OW@wIjOa+dDgh5mXb<33XPCGzqF-i5AQn1d21}@z_8QzaTYW zAox-mjAU}$;i2nCIa$@3@?xQ;{{y#M4w-#@U8YC46zdBU-EgD*?95@bUIRVgix?^P zUtw03G4u+#t73}$B`pc8A|HOhFFTatT^MO5x_DJ*quRWmmJv!%uo%RMYbooj@ES1B z9}s>kA|%BLs-OK9>|adM7qLXT&NqVWK|4$^ozhi$hO{ROf%pGBSJBUHes(zRkk*B7 za5>Wg)AO_D?zDUl2;7T(EAMD92N!3$wWx9I8GLRFHw!jEp8AT@B#I4z`9hwE`I9cV z4mu+0_lik7xUj4QovJa@ImN1W8RxNMUqr4)dF(^B=-;FiUn()v?#Y zopUhAp8U2P6^KBAMVrR|QNZ&Gf9PG`sMEG;%_L{zx`NrfCPci+O-VQRs*5W%N&8IN z{WgRnm85cFeQ7yD532Hm45;Q?CRc%mC&Ew5(OK3!u`Nm!q3r0;BCic)cO!VnvvxB1 z1;vqiWKsOQ9<{U0>yVN`CT?6kfyR&R9_KUs>CRKnqWc7hE(Ee9Yw6-|{9^KBX{zA} zF{8QmxYsVXq?n}C-1vBy^>qULKSK-6@oRYI&_u7Iz-q?j9i~ZeG$F5;Y<>O5>XDH< zBs$Hu9Hmfa2;y*Sozd_1T=%NNM&KTi0D8QT?)|Jcc#D2}Q2Kdzb;j-6jwEm8VGN4M*S~#c3)~HUqyOr0q+Hy@-IPpRgxt1f!n!=E zmd_w8i?~hRhdpgmj>u^m@|cND=$|pJ))mF4{IUa>0zgE;7eH^8&#;A* zIw?^#TC0~BsX|B<6$u6$5R$g-88+0k)6JbF@Re8M>@bcmDy`oC^|P~ZxhPvDdM=IO ziMJ|rNFJa4T1pqRRS8MV|Gdoc`X{*lo~o?IF;>IM8uRuajS(DFesis*PHZ~08XVDC zZmx0wy^<;|+@Wk)g<})?7h|SXR!4vJ*C9ZZb6@%!?~;Umf)AAZoT-uAfeiqI@ecst zHLCm5<@6zX!a=wOrZ{2aOU&1D0TahMI5@i3=de_uEDxo=yZg(9r)kyQG>F->Gfl0F zfW?Sbed|gDW|9bgv*UxizkW0W={(QFrIK9N=7P6yDj|)Im631$E|WB zEh34ww}`eb_XWB*+x!UIZ?pt`;4KRxE+gE{^|OR?U5IJkb;&F>R@5Oda^htR{# z(`jkn6Qe3LWV55Hq=dCJMff`AaH|1Iz3soYSZ8@zM7#DEtg$V#XKlb9|370(_eru0 zi!q?FkAjIsMDYD1z{%PCef?aj#is&-z*qh!W>lfzczRXXvD#Mm=1lD^cxJ|FT05}! zuz`<_uq4w25j-^oM98t=@R$b*o-BsSLvgMYwP@tMf7+9p8msLeU@`D`K`0_U;lZ;+Vs_mlb3Ua;!2tIRhpau3e`oRRL3I3pe&}>WB9>~wi=o5( zKfRO>41#wT+B?D@9j^3tp4$s1FDMX>NmByxH*`6q|DId$-Z5TV_w`J#&dZQ3P=F!h+`}d^?tts$=fP!$8xL%2&lx$7 zD&(bN{agG4M@L8CJTdqCEub`SWxJXSgg!8<-;S$R8M|WSQP);i{C*gSH^`QQFhOA3 z5&a`H`!1}+#FYRsxfaiJ-H}JJ?1giKCk=rjVuw^`bM__)-2(>k-x%KgR;)+q^huyl z9e(rS)648viGkVETgWU0inWT1@G<|J`W*M1i(TelUb7uP^I?<{w`$OHNYTLo?o-Iw z@?i|Ptmrk*7r4~%bYzM*JdbXdhtSLcXX3|?W{*7yLZD1~x=k&~-!_%MY2Km>Vs<7w+*L3Mq(>|F`edLp>$uD(^ zUMV~>uz^zg60A)UG8M7bW$&)8dXoqSdWug<@k zUzm4U!r9wC&H21)AbG|2c#>DrB&z-vRO`sB&4P;M)_m>DzWU)Z0Eg@TQ;RPGs5bxF z?x(T6U6sjZ7STR3N8RpH{?Wz}8)!LyHE83LMe(imK;+qe3Z$v*Q{z#w%e^i%NY&(C zh5n~$k?T&;Q&)xf4+xwJ2+$`?!WixDs?9Nm&;_F_3iI|A;ErPVvTs@}F{Um%t!yec zBskinwFPb!Q-?wzkm-Bi@xHp<3cW`H#MM7>FHd)aL&HI@$x*H@70(k7)-c=EKUn8D0^ZFRTJ%138&@jpBqjZE@Mnx!KHcz^leSKT4c5 zUfM4?Y=)as0aT_j$ThTt04goS2&@y1#T64kNq>999*zzz6kok)n z-!rtNBHw5A?G?y&P5PR7c|oo8Rs$?HwQ!$_r^D=<^=J@i&Z5hb-F)6@2;t`Q76mQR zh>V+)8$*j6BUzWbMKbeSSCBF+gMYEOo ziN%Kb74}bstwbkhxV?Y(J_yu>jMQ(@esg^`r;f0<5eYnVpHL+*MDer%U9q1MV8RKH z?mfhaHCyIWA_+VV4?Nv*c0Zr{s6;t$O=Ene+C{0yQh z(#XM?0Q3W^PV>nECw23vV5v|Y`soy>L66S$L2~jhxK_IoQiCRr? zH^VDX7fN$7Lv!tv>#YJ}G;{Oh?G7Mg-NwFnXN%x*(NxQJSL-Ea4GOx_mbH>*!z)n` z==&e^{*%qg*gG<=28Bmr!z;e7B6zwqLU~UH){jxGZksm>FJ@gPvxt`Eln zy(`P(Jr_r8B7Lb_^a3(^iRl1(7#sD%Z!`~HH~Qt9)LaL!_*4N!W40dadVFO7AL5#} zH-5$ENxq-*ODyBbgxm9}KJi(AM1zVUY~gkxKG0Dch?jF5ktc?(114L*;0TRRyKpC_ z`g;3+p16GjwO;dTu?+HswotLud_O+(Smt`HTZlw2IW$o;xcX>(YT)%fL^bM+ZQ}$o zD+c6#;?rUICH3)a`cZmWjGWr#)Su$UYf?SKRv>Z1){nrmZcEijE7L)X zr5tBQ<^2?~a<*%12oT7BL(;Q+xO)17xDfp6BIfyI^7qz5bhu(Rd#`3v5C~a1_H*U> z99kli9u1QrEPV>shnuy1I5I#Z*KRcOEj&mzU!kv*z=GU6kU${N%pW@n&BSH4+ zZ$2HOivWK3QI#(Vx{e42eF$P&vow{obI9_Z#X zl1NZfDq4QY69Ilj{{~KrLdiK(R0rmMgcov06U_tdfmv$*7pBJHnn8l>yaW1uI1os{ zuLDW@h=B6XomUS;U&iq7``MvYf8FbI(KRw#IrX|<eGStpvm*HR}g55-?E&iOR_ zcQ%(Q%BGo6dO`K^J8%@Z&aL;35H87Z*={ zW4ihG(lqM(VV>huXv9Ou8-ZK4q10USUvt|OC>thr6C&U(?=HP7+z3{3_#e3!X^ZG+ zRyy0pvXe7vVoy&!VQ*+ASr!4&=%B~8(O$AY#gsI=;`qlsYM;3wT^)+X2Ii^7G(UFq zHJrbjcCk!7<;q|uGKdk>K*nk4%}Iu!>xRURv~;DmnN_y@exz!ZnDU{+Rulf>b;42AD6gC{ zY5mT8Mnr|zS3r$s+Oy4rn&MmJHFEvN$paTPJ<#t=DU3b~?lT~`JCTF?m3D-ii@lcn zBd*;sa;{pY=7lo36n#Yar2HtBK&iu-=%1Pv{NNrD_6oUO}+^Ej?YYE ze!3Q~#-dn5#h(F?g_5AwTTJ$|Otuz^a*8K6I6wO;6-&;iZj((YWCR~*Q_FyL9Jsm- zJ>kwj`g~2^^ADCiM8!qnewTRW3><&uYUIDTC80=oPbgQFFCX!0UIfND-u0EXllC#s z3Kyb-Ks~I0-u~!IN?;J4Rg@lmFw+=qCP#Nz1W zsULS_RFk2=c1VtAy6t2kLhT6Em7cn#A-AZ#$)s&t@C^uLOHlSp%rp=7aczVPisPnP zH)SLgFl+n3{O9arW;p;^go!zKgAJn+kJek@Gm?dPWx#vf^2gZy>6Jhz1Ltnks&J43 z0z4pI8J4hA_yRqs7&3L50~qT=?MHpT)EZ zj$!eF6C}_WI+eHH$lOt|?XPz(YN3)L9pxL(pir;?M1OE*6oO z$_rngs5w)pEXCD!>b_ul%DE*`1unhgWKo;5zgjAGo*0{$sjjTMZ0gl5X_O_MDB9RE zYS~1~rZaD$?ec_n!@zXWYHt3UJUM+9BodTf*(Jx5-CS_C&cSgJ>KIjq%Q`*A6663Z zQ4k6+|6Sr}bvP!m{GO{Vl4`X$_V!rnlsS$*qgn%T?-FHO@(Bha%=OEIcY03(d8+G( z>)0}LlW&N(wLiw9?FYaw&l!GHr(jphUk|>OJn6y@=hztf?giwPwhf`cdb<-L^?%ro zY=|U}v1K2@3N~w$U@3GEwq<5ez&cg)6_q8&1aMDl^-AFZ_rJ)xR%Ba~Hs#G6)u=`k zbd(w{1(juEY{04qK@-Lh(9t?ntwRnirheMsl&atv;ua2NOLew9U>$S(Zd!#idzFBN zK~bn;2`uc7B>Xb0S0x>XGlwWQ+4U6md9@7q8LrrL5{qd5)?yMn7QI_%DDj5Pgz$#8 z2A}mJ9Dma5SCKLa?u=6bOR@IBe*>mAz{&Qi+AiSl&VJ!sf$aVff=1tmlM5Tc4z=mg z`4&NW!mBv9>S8W^@wdtJ5?EYvs49EKW3kCL4=bDfp_shc79VTmO=sdAn?C{rQe7@S z7Io1i<7&#Sny4$}maiG2#mwk(u+-@YSQUye zMRl2Z8AM*+L6NNy@B@0f1J=Z*M4)W~0j+pa%IvSB{l$ZkSLfqgyWxnY zDkq2~)RyEZCL)POONpwNeIn7)3(Bs|mqxe9PQ;sg9G1BZm+WguO84r=y*q|ytq-Fp zqCv8NDyMsKB-SC)|}=P_MsHor6@oUiP`?P1Gd zIyYf(Y*e(bm+i&msBv;OELK8}`+-9WgbNuz8lLSI4MtKKR=B~9qc9G~n7MjETmm`5 zO))QVz_}1_pek(6xw$DvmC?iI^iiV3fEZ4m#3AyE2P+6L=>l3T7$ag^sL@Gc1(Hv|=@{w7(WPw^CLE z_1@)6p-b>6*u$!)Ve3bNnRnokFmiNWObm(?W5fjJ2Ov{Xg5<4*0V` zNQb3!+=NBX8uc2qFR*M3LBJ1RR>_!w@lOIvsQ(og_W;JsItP^wys-}!9RP{w080|A z{l|OOvfq7zI#p##SYK(e|0LRxRE$(7l^w57@(zmOX;0 zQKYKzx=cH%ynW2yvXAplrFM&B;#NFG4j!{FN)zjByJ4F9lWG~w)D>%1al14>YO-^7} z;;4H_?vdYUHvlOUXg-z{;CayJSh+#9W$7Rnm9Tp1sisBt#qjmwbL3oRA7uZ<+pIv! zzUwQ_?ju(~l^ojb=Syd0sz$Omr)?yoUGqA-3$VGLL{wpM{Ub84 zof64?$J!|cF0XR-WzkwFVS9@l#TtLqo~eH@WADOCcP@#>$;TiAue~DRm%=?`T}Ls> znCI>qu=Irgge8tGN7@nvvfphZg&$un&fgMUAS8vAld2EfZ$wa&0u5LaC!@Vp7NFxIUBMh^xYg$gNMga8=n? z3sc$Gnld~^!Bt@sOf_ONrSnd;l5DHwlc|ceb^GpCMUG?QjcE+(&rvZ0Dgmmn+*3$+ z@r$2&iJl#)3lwc}X+a=!FN?;!=1WSq2|9Rm(+8X1y=HQl!L`x_j8JyI*y2{7#lX(! zuh%cNko9lUO&{9vqrQJBbPJ|i$Ji%Mj#6x$m}Fl%L3%alwT9S|*^PeU894Z$Ji%taTo{WrTaiv~U5QPc0LutUX;q|uZRUGx@= z>ds0kUF2G$WapdMt4V4*LW}vb0cV>eE;|E7&{A0Ms4DMhfUYq=`0HK{$UP?Q!V%&G z(c2B93VAMeKkZo0VD=7pE!w0$TAdAcX2)oPbsiC^e_sZ)w+6ZxmJRq$=kAwsseT$yL{dn*x;- zha|pz-|yVT+v%#xjzb8*(wZs~#>9_G3llUInkGD<;Yy(G>>9tD*Ld-JuGyAk01`&JTf9dU4-bcB-F^EpD z+>qNpCghJR7yONxfMO75eI|Hsnjrhxkx?-c(^Pps_A2D2U95AX+$e9c&FZeMa?Zri z@<`3~4yyaEMaX0&5XCV@Xr*1?7Yj;-HpEf|b47$W$;E;Co9N!GCAv{he(Xu@fPjxl5MWAHk+qyW zBmm-SQ&qq`FGpX!&WRS8o!Ful;th{b>w4ph)mh$Y&5}tRQ^x|&X%IRkVAreB_1b&j z&w%Sv*&tesYXc&X_ta{htS5`Qq>ly`ucj=eup+F}s2Tu-bY}I?B^v zR-2&z#>nq2TmIId2V|>ZHh>9WoH_*l3!3)opqqQ0i3?c#Wo14sme(q#pVUnNS%60Uk(g!@u*Q(Tf_%#s^&8Aiyid8`2ifd>@CzZ>Bf!VqA< z*p5*AIr=NI^t%@~g{#I%3^R7>?7cq#_diqt;lH~PT5Ox(s_Bh|M0|p@IsgNm=&gTj zl%2WO8`Q0 zmkVTQwNL;^5A4dWS4jQo<}&bM{u28DhSKV)3JYv1Zu$Q&RPxeD|A&A}-;B&1T=aT3 znuw~I<`EXp>r95W_XFJ(H4Qs=E-1v#-q(zMZJK}ee=in1(7pz#iqw)j^W8F8la@px zgxI@ZZsD)8Gp`7XIVv!c$HGO>=meD%M;q6|&URy$ zPWH$+JD;W6yY5UIugfofOlF$xzu(#ymN=EqmQG^g+UdVGk3(2{PdD97j$o5v!G>51 z4eAI}4B>maO4@72F?Aq!0it4VVu*hsa&D!BvVZXfqAe>+J6?Mb7gIyG{`u!kgiQGm zAqh8AdE%sO-i*r$A6NuN#YX;!w5t<0R<%n3%d<4Qh1{q4Mk|Xv-8d=q1JD(&;Yp}1 z0?2(23fNvRX!P(Z`{3LbUEdAWdC{3Z$ymfJNzr|MjdAHBIu!cP`;n0Ni}xOy<9)Px z>&G;M;_8}hO00yhgd8$o^HvW#pi&j?!`4Oaaq#oMQz$NWqnI)b1NT%cc)8R`Dgtu% z=%AB{t#La&ViFq-SQ(?&MiVVK?ITi^k|O+{uu|W4x=(W>F1gqx{QvOUM2=}J_pNlk z(mN$LIbZc#8j5SUE+)H$HMEH)_j`kdoO*@y#*iC`L7vh4r^tyv(lv<*bbone@lAlMlx1Dl7vR*(r?l~b| z4m!;mP$>qF%+;7mc(Hn2A9ex9t9*meF=$uQM?-}+&w`r(f{dspc`y^1>l^aOP-V-& zY2aIq^{UEe>tTn%dr!seu7$zd2ALQ~p))aaef;$``!<*^7s>P?9;hTI9t^gOHUc|h zt5zz~PM?2}mgN{6>}H+1YJ$Plp^yKi{1 z-6+5TLJ^f&)w{*@as~DLsDJrILA1vm8-`Pgh5rbE$IIWh?*+~a=rt)-u1QHj-v)-} zl4Qhrb!5Wf@Fi;6jDfMT#6vy;$e!e%Atg+QumRGn4iXX%nK-)pz6h6bW<35T{$l?m zL`dn=Y=`eQ{hMi6<>^1E&&H<`)~#DiuGAm13-&zkuoKfi5TN$0#_4b_O*ce-#QVVb z$thDyP!jX6^>~DeXpo*>`s7$BYvm^CXT6@50K(S>BVz&#i@0ZccBThzHp0Q(*n0&> zNfvNH%w>p$Wfi^jNP)yBb#)ux>lWMqBy0`R&RcK0uHkX~F51jwst;Q_toosC9lhE4 zWd87E!}p(&S62JKUF zr+6CFTqN+B)U~nfyN(3#<@vM_wNefT=nG1u#C<#|G1_9RZ{RHx>zCE*LL?b|Rb2Oj zLEt4Vo2YEqd?qc=+${g!>QYW zF72L7J&e^S#_j~~4b*Jst27=_DJb|1j4%gbfe6NhtZM+k96OzOGSb)1wH~b@BsM~4 zID3yutL20im=#~>{B7&2K$5@b#3wy`JH*;X%7WyR1%`tfBvwvd0-qahWeNN38{$Q5 z8FwB8yQH1p@Wd~nrIOEnDh^`Q`3y~{;Uf*Y<4S?Ka zJq?RkkJa%L>B%J2ZA88n8sHKIzQP%S#Yae3qkJTZ@5)*O=jdhArvvTJY7GSM4QdiO11HX(b%Tay1QD@W&l4gvy6l)7 zT9APa2Ztn_Xzp_+*2tCvBK4hf{6gQ4I<^tS-?L41({G=awaweeWm$7yn@e%v5`YV_ zhiDv8N$g+8e-{9FVr&5K$l-h{m~y>n(<$1YCX=Z2g%X$h2LVqW{1*bA_of_paMIyF z1^t7)5{8wga7~-c!}HR)%!V`w*Dm# z!1+RY`{PJrf4CkTOtDTQ!h)V+~K>|_d6lOt5nQ4KUX=1r%ox+2KV5XjN zV0T=h?syg;1U~?E0fex~O>V#Bq85P5h*H?atoqv}M4^zYjHbEa#B-9F5m3CcQg&kA z0Vd9sXNyZ(_$Y8uBr89469E;Og(@^w+t|g@fUhuzftVMvlB`bu=VOmw3ajD%!%0E( z3GZDGpo4rGO-(!IDsDg4=*x|fQdQ(uwq4_JK;|6(K`e}_Y-%s2(sOul$dPbrg(O73 z37Fyp*Ph$No=Vsgu9nw&5nzaq`?Pg&bsOH5w&#Ur0#)FuBbH@JD9gl5LddcZEd(vK zaGWJ+FvdZ6e(ff)fR(y0$>W2N-ueBP-$8P3*Rm-?O5P6+iNN?Q(OyLcEyZ8LqSt+A z4A_w`^V(nL)t$xHy%PNpkwFr-n0jtE#Ljj`Tl&~aT&ABfSY#zwbyEFx2o2PVXBsC8 zBzXku0S}p2Y@P_Q1`7u9uUSL|=MESz?0|xx*g(K*O*?128h$|A5UwOqc236r6;@_UtP&=ib z3jJd$+}gRUD=SOC0INvPOog?#lddw? zsTPSDSMQk;;B-}N`OYbXQJF|^PJ5vaAUn~IOEV)4zh#eH&da=Qae-{Wof$~~)b2;T zNx4RGcfJ5n?9=Mn4*u47bV^|PXT^)|o$~9Z<;bzBV7sS-J_Qc70?KGGdWV8ItVYWUq@G?K^Z4b;UWB6sLX3eyNB^d9quV=2}fV0TVZ$0Hni< z=0>ySWaVnEMDCkNEVq7l2Hb`znzGW5Hgtfq!SIjl5dDA14wo^{eta1V_67-WW2K{z~qoT3m zaHcT&ZvAjQScEM_Kg30-T;fm$!6+r-qxb-duk+MrTHWvPZE+!CIcJdpvTu7 z27R%6$m%ll&t1{}#1OPF+K5TcBM}Eya=$}u1wEnC=#IL%+$f^?KM;}9NcCcaUGR66 z%yo{vs26N#VD2*D><%(Qn{%pG+0ZE4ydclgYUb6J^2K5D1i2%LEDABp7!N6JR0IF@_M;{Yk-}n&e*3xT#I@s*-3@VXX{;##p z%`k!rweqaXZ_Se%*DJAA8-xbKrrX8Oq=h~gh{psX4;P5&lh2v|szV2hYCtzDt3kRx zR3#5%-CGYAx-Qv!>s>4ryI%OX^YRQ=7h%dL_q_B*sr8bRHxb`ofGVVeFVe_Y1dg%( zj;^G2V;BW(eU>K!izKn650}(+8uf!jK82=auGM9^|4(b@9n{p?{d+trsECMkX-X04 zf&$VNkX{6&g>FQ8)Ig{rHkve*5(T7pBoskPARMJguc0^TH6XpieUjigzVE&Bn>%;z zUkn*Cd+%rO^{lnN-_P1~E)D@PnL+GA<5?%nymp1fcDE~3pdfvZ(e6BtkWj{3%C6}D zje$BHxbt_7>hwA*=tFww$+eNZ9?1Zk(ry+7`$dxiUe2O%WUDO)_o$W}U<@-aqq*qz zz8l3v<)%-xU#IXK`^^~8%!d2XmC{7y4JJ93On!-Y5o@B2gH%dv>`v)9bJ^9&mTJEy zn*(d$KxG3e?>iNeLlRu2B4xANLMGB-%PrjTrf9ADJ~^F+>GTw*jfyM&q~zmKrXXBO zuHfBFrf&7y(}D4vh6fnvCn5g*`bhKA2|c9X^wQo1+!yza`=PGh(zdi$#M8fV<6M{U z^5iMtwkYVuA9gvgVXjG}>Jkv8-ySM%?1P)q1i8oET-+aa&DKsC$E+1|O-`hVYnCAv z933Sbo8N?W+NGXk9(PPmFUfy$8#Zq06S}=NZl9SG`8}t=^Ul)iEcx*cS3nPh9nk~q zgfcF5Q_lj*@flO<)ZjO1j@|^`lw~h)Mdd5129~c+riv^)R;$a=pU9%6ZD6yh(X&D- zWNyV2F`=_V1XELY2y~J_k4q&pyuN$uasK5`YE`a07rTKRNK$(oFMkDg3A@bb-+3|a z)miEAGqk8!dIQ07@b%%+S@b5}#R3P*O?yA;Ms#I}K5LPGH9VGw=(8&QhtEn%9lWO0 z>ApwUYLqbrok<9mAz<%v@SuVYf5z_s3~_IUig?KXa~ zBrh~Nevk@=NL-QGuX?lpmrM2}ys94Tv@x z@thQM6|zLeG1E=Xh7oK!Kj%2WJ!gxnyf&#^%fTXkBKzQ`S@xhiUbKiqC8!uqKZ+{+ ziZQv*Yg@!!M5lX%AFv;(6qnSRf##8Q?t+jD;_J!9T$q5I#jdCizaJ*RRhoA$29aVK zB&X9V#N`Howq6$`vJ6U_yp0K9*$vt7+QZn6#7NBQtDv0*-}>uMAu#$ry@TKzGm)ja zC$d(CMd>vJzH2)3p83i*-yVgIZJgBCu?M-`H83+F{Tfv-TB8cfmm4Ji9*=XFP1Yxu;@bOt5YF{!g~8kV;(Opv=#e zuZjKq>qr%s5`9+!eUN3cthal`4o(*|F z>37sr+e0gwBdcZ?={RP+^jx!oM(BJ&ZoguFZO|h(K%7; z1HUBD=fF#ePI{)7zsgq&`KDfzLltCD+4v-l<}9#y*l}uf9vkfn{^u&vCXg1oLW%wn zn#(V1iA^xO-`B?yKIvG+U4GWa@MLo~N=S)azqE?BWt@h`_-iPh?5J?n-v_uJ#qcp|BnH7rqxd9~&006yI-4Iy?4zM$j= zpKd<^K!!cUz;2_n(0s7loXF(8A?Vao37yDM zwy!~3zTlTEd-yPNc!)P+=m)6C2N{k$prR&OKNBAo`xTQeB@PreE9Bddm4BzHN+$;A z``79vD7CnvkZWt<$I-Rk{#$9C{ zXHFBhnBbJ#gPFc5`k*zudgk@OW?FhSEIE69*Om$&LG}Q@h#G6vC08Z(Axg^wa$LG1 z3$n@QfJ5-&n@h7p+kObV!vIDd zM0T-gw_#a&6;vKriA=#EDnc(R&X=4IEx$Ms8kdyh%V*dl7v&%dT+HV_mgi!(Bu9Vq5d%)lO0m_Rjl3`l^M9dXKypAd-7* z+#r@2XhAHSnS1r@A8zyr&3&{65^m*|dzs3gUoR8hu7%9S|P z$dzLPia|~b7QjG1CN{e@uRNPH87P96*y-NESTz3SaKwuUrxuiyTO!;0ozXv#@gnk? z^`xcVyWjgNRSQoAZWQ9{4?0kPbjDeGk+acpbmQrh<=f1|L2CN^_n%&@uR@*DO(1-L1puIh(G8;^dxNpU9^O|oB9&*dy%0$d_Ip1 z(a3F&@jM8{QpElJ*{M>XBZhP6b0^z&xO;VkUG<$KGx|6H@nAOrywK)HZL`x7yYq zB-I^VDF~r9?6l8%zrT=1cf*lQy?^7sY-;C!ZR%rPbfF&h8(Y588HR`+#)0ocIJq0V zEW~PN`pT<{q2?GZxj5>u4>u@Q?@hj1bn1AxNd@{a@rxj~=DzL+u5@}qOg=T9;Z&9p zDkU}?j)GD@O|yq9lDh>7+sqn4_QAekUgc?W55hMgD*A~}iqHov$#xldY9bzK8F zM>UTQ*{W9VN6R<18XO3B?IY#tAU07u20XG4LXL1s^X!~|`w~4hRthw8pzq}gxT(j3 zoIAU5$16v4WUwCN8%{zroch)BT$&u0LFE*1c->M%w6)lal;qm8p=%%M`hL{iY%Wcx zax!P|lP$0hO1f3zy+pg(K=cKb;MV*Ron_g4$y*?}jqmJOeYwCmMkVn@@|?$>wZf-; zAWi161Wvje9yKMkByPT8k(M?$a!}hPLtnrN*IfkL)<8jf0q|L{X@yNIy(X@$AOJ2o z0CB?!map&Rl5?2YLWiCPPsC%|WtK)qa-;5pvf`%ox&w3x888n`D?$Bd+2}OMK4h{{ zmP{61Mal2H!1Sw!|Mc3@PF+!<;s(V%wY9qn7QqsKg4Wrn zZZSN85+?h#ZLtEJHUlgE_EGw*Tf|{9=%>Y$6z!#9nJ_`06bu-sShRpwI^Hx~Uz2`^ za%S&ZA2H?Td)3fTAL9J5i;SyZC*{hr^4bCQs^)lFQ4X<}aPFnXuLeC(Y6Ve;JDXE` zw_5qxBE(L|AGBV8K?r=ZJ*XlkfGHv?13fF=ocT+?5thn8C{t8au-wxclDT1%1JMlP zIXMuWyrF-alYimVvfN5sb zj-uub8G7o9p_Ran!#$Q+aD4~|&@+`smFCVHuPM7F zWTeZJgTAr@Oa9Y?BP^JR1NMR$Bs|UZ$LL7(xUJ*f|jcvOy8;#sKP& z4|rW)75(*aj88~Qq?@&`RB`{}1hKb}xLt(G(OHQ@yeTU|BM;Lo`!!wlHjaQyauaNOaF(nu2%`YOLG(B_I8OSr7(xcfb~}3ajhyq zX&9)9INkiCsdu0VoGj6+)_)A{kmeW)`~bFkpmp~RHt{p>4(?*hW$)vGPEy)n(@3l4 z;Gv81xz;hZO5Vy$OaK81QXfq&v`%Qa%0e!PjUx6H@=s;jzn57Qv=x*ip_6#b^K6=1 zmxJLTj1|F96zxyVMsU2l!FAdv7>sU-}}Mf^D&!`9mVO>`6h;%fx%O zdm@?FN}cH!wH#tx2L0%zR$IUwZ^Bc6$l}ALT*r?1@-I(r2No`Y5eE+iwM~k9-QG0D z2<24W5j<}?%V`%d)SZ(P)FHp^y!36@LtCo(WYLBcggg-MJ12(;(9GgYiqow=D&*?? z2%z7f*m8(Ekwn~8j`ovaU*?ZE5sEZ%VQmqyIm;^9&u;G`tn`0S&iAy142f*$&m&sk z^!ScQdkzG1XGwrrq@m#Gn>Xwn&t(npJ5ij7rgg@@v^UI=d+iowG&J6G9*Te~*(8IJ z83xLmpaf`iAg>gk&lPf*JdIF`>Rb)G;61H1D|s~Hm3dG_P&)`J2IRW8nbT!6?rill zkP%V(LP&-0J#kvOf!^d{wN*Uay5w9`d$@qcqpdyl`|69xxv3T9IgN5d|U{=hpvG zS#+O*OHs`SFaNP$oKG}5x)cyUh1)3O;qQ3wmkX)T9tMX@$WJiMW#Od}%_MfOhWK}+ z9sqvLbVV2CqyF(`?WthVdO8UdEKa43rKaZkgr1CtcKKz}m1~!Y;Q19Wns!&=V$fTf z4`iJ&AFePmA@Zy$l#mftqM!u2J(AOl$qLUOKKao_w$#R749)3D;>2H+oC_(OpZI4) z5}uppF?ZCVFV?0A>*fy!(?uwKmEjP+^=_bg3jS~+x??11ge&LKDr+EPQr{z z?;AE5(CY1|>EB%)sGZEIg#t91^-aXUW;|QYk8@9w;LX2d1QW(Dv!;kx)>#q9&*}|% z^M71|3|zJCf3+O=#X5KomgNxU+^QG`1oCg-H2X3IpGld)y7=Vt<(ocjzHv__ z@J$W9-v#1nQbh6RV6S7FXSFWKQ+3X!_yYRU^peW(1n_Q$xnXD8(h+MZlX11d9ru+9 zb(w1(IQHLKK*H|NGe0A#y{BkVy6aOK51+)%#5{65n|7n+;{ya8&P}2v;^k$$Y&3Ve z6nLG;fY+6qQ*`UDk#BWZQ;lv$fsYDj&!=maDJi z*-0hf4DNoecDtSA%@dXaX7aq{l!Z3l>~%0JGxBKpwbUn|wVuaueZLj(}_6Vk=O zPAvxLDZez#uTclKg9ine0{4U^Cb*$?+&iJ|oib`l3~{h+ajw&JaUm_zPxc~qsO4M0i4xmV!CiC}GU8|Lcj2Yr6j4MGyuOVmWerzz(VT<`+hcWm%1$cVVG06zB+@Pj8Uwc%i2hP zU+P5)aYI$nw}Pp)f~oKU#g(bwrs+kF=VIMcPF^+J`+s%bo%+Q|| z>BmPv^h%IDvC6z|M+^V>OkJ2p%d4)r_UB?=m7QRU-N?qv?V$JVdBP9M-xglJ*uKal zLf3(reqNRIsO+1csDYlJ=CCrfSpL;H)S~=!sD027IpttVmtXK2lMMx@eSn3LRdY93~s;syUw=WXF) zo3b#SF3l;lz)l*IqY+fpx_83c#bm*uU$AhgNG-+;HGw&|TGlDjt`(gi@s+Q;Ri+yA z7nm5Xm1~41ZGFMYzc*&_mp&+HkM-co_o9TNEq4_`H|hii@)a-TyiLitI{vOUd)|^B z1jN-RhVCFAhtVekna?^#T+&~{_L@l3IcQ;Wg~6`)49w=LNW%g6LvtYir3xI+A2};W zXPQBO1gSv^QOFL(un>9+Ek<^yCeVDF@wk(6aEdPd$(-3Od%f*`H6!Jj^u%i}a+hzx z^{1q`3<}C^!P2#>bzP>hyg#hm)OOEG;gkMzko2*I z(>Uj;2^%DH?4?Ct^6QIK423nhUX7g8ufpCoeR*tUZCLlobcu%-QUXWpLrWDx?Z-SH z9Gcaq^OKD@1qB+F{y8a)hXAR1OA-|OH}~CNmS0Z&jeRS4uOOzuNK`7GGc$DF{4@6| zf>EbT^OzZ{8`<~%1jDHXKq;RYQ<)sfmqb}uPxEw!3Y+tTvNHC=E`b$~X=(ovEZtV| zWPc=5-7!thXSZOJ$6|-bi(^Z0UaC0Xr@|qyeq2^L{Iuac8r4HoSj5JTA_0euFy;=k zRm6s3#k+oJ@UCqr@h5+2KMNXkhr#mjOu4--ACH|R^VqxTDRMH0SKHkdBT&u-TMw7Q z8?kgu(@j~F=jvV>AyMJG$U&j5=E*Q@u?iymu+OciUy(AoPoijcm9qJ>_nf@(w0a<2 zdnmgTjgzS`sR$Vx{Djkd^`3jx-PKuT7Rc%x_~%(nUSpdVn&0|FZfQ6y4Wi*?H5mk# z12KMj945g&)vk&5#;0bT&@Mk`eeKrkS9FQ_`GZIjm5!8dW4fq;Z)BXO7e`ckpDpO8 zYDMv4rPY2j8m zRskgef_)OQqdHw9?b!ii64Z!G)VLiJaIqCkmWM0fV85t5N0=nf4`j*@ZdoQIy>YO) z$5q-yHN9uU=`>m(DbpJ}rK*(ExMUI^LNL+k0IFJ!&Mam3?!XU@1Itu(gp|#~Ztg99 znmkjGznHT&gyM$E<9RCG2^>nUU{=CAZ})wACt&9jv=><)5a9V`iek<~EZUfZj|@n~&E{TVjYquA@tvrO7E<~ynm)tOqHM_JdSx>vAf z(w4sW)s)fgApC7H1q0fQ{oU}irD8T9`_fUac`Gc=)n6Ck0W!+iF^S(PaUtzd&)yfr z{x{d@Ch8yBH9pcROH>Q`uXDyT1%T-L!9MuK`c+_ zecF+ZT;VMpRrVf>4`35uLbaus4frfa(sR%9={<;6r|TKXrEz|Fvl>5-t0iCeGjo(4=bQ{d!z z<~={4hTl^9pC|D_7yS>T9>@iLD|ktG8Ondn{QoDF&Y{lF;;IeW>Yve6WjgTmcU(~t z9afvwc$bbU_a?B$zxO^4%RoBx$C+`=1H9!i^mIi>*%YY+RO0*EuWedBT$}_b14O@- zKEw>^F>r8b@XPp&uoZa$6PKiiM0t?{j)7MPrrwlxZDj@@jh~Sd2cpqGuORr()8UNz zf4YkQ>#vzgfFTk=L2NNz9l*PoWHYkTDd0 zqD{W$c4p3>KIJpBJ3W0?qlUIQ1y;cfj{5j$lcvBipt@sqb6*FYj{lSeK4ES%&F8Pd zUo)ETpGj#4o3fH>A_KP_^v~b)mXKq0);7NndX< zmz)~eOvwFF9Pf2I;5=AM#ZzErf&Ddj8mPWsAbEbu>NLr%*Lf#MF3E$}IGh*uw>I-6 zB_RoVmg6MmKZ){;V*QKc@05YURPMd>k8uN4xc-Q^zesL%xrH^VY4gwTu6cfvO+^9# zrav|e{Egk|MtgV4)N~S(yInZ7Rz4rY(Q)ntb)jP literal 0 HcmV?d00001 diff --git a/event-carried-state-transfer/etc/event-carried-state-transfer.urm.puml b/event-carried-state-transfer/etc/event-carried-state-transfer.urm.puml index 08f2bca14a43..fd6835026dc0 100644 --- a/event-carried-state-transfer/etc/event-carried-state-transfer.urm.puml +++ b/event-carried-state-transfer/etc/event-carried-state-transfer.urm.puml @@ -36,7 +36,7 @@ package com.iluwatar.eventcarriedstatetransfer { class CustomerService { - customers : Map - bus : EventBus - - eventSequence : AtomicLong + - eventSequence : long - online : boolean + CustomerService(bus : EventBus) + register(customerId : String, name : String, shippingAddress : String, creditLimit : BigDecimal) : CustomerState @@ -44,6 +44,7 @@ package com.iluwatar.eventcarriedstatetransfer { + changeCreditLimit(customerId : String, newLimit : BigDecimal) : CustomerState + findCustomer(customerId : String) : Optional + shutdown() : void + + restart() : void + isOnline() : boolean } class CustomerReplica { @@ -55,7 +56,7 @@ package com.iluwatar.eventcarriedstatetransfer { } class OrderService { - replica : CustomerReplica - - orderSequence : AtomicLong + - orderSequence : long + OrderService(bus : EventBus) + placeOrder(customerId : String, amount : BigDecimal) : Order + replica() : CustomerReplica @@ -86,8 +87,8 @@ CustomerService --> "*" CustomerState CustomerService ..> CustomerUpdatedEvent : publishes CustomerReplica --> "*" CustomerState CustomerReplica ..> CustomerUpdatedEvent : applies -CustomerReplica ..|> EventListener OrderService --> CustomerReplica +OrderService ..> EventListener : adapts the replica OrderService ..> EventBus : subscribes OrderService ..> Order : creates OrderService ..> OrderRejectedException : throws diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/App.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/App.java index 98ee9f57711c..3e3de4bce66d 100644 --- a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/App.java +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/App.java @@ -41,8 +41,10 @@ * *

The demo registers a customer and changes the address, showing the replica following each * event. It then takes the customer service offline and places an order anyway, purely from the - * replica. A stale event is published to show that the replica ignores it, and finally an order - * above the replicated credit limit is rejected. + * replica. A stale event is published to show that the replica ignores it, and an order above the + * replicated credit limit is rejected. Finally the customer service comes back and raises that + * credit limit: the new state travels in the event, and the order that was just rejected is + * accepted from the replica alone. */ @Slf4j public class App { @@ -86,6 +88,13 @@ public static void main(String[] args) { LOGGER.info("--- Step 4: the replica is enough to enforce business rules ---"); tryToOrder(orderService, "C-1", new BigDecimal("900.00")); tryToOrder(orderService, "C-2", new BigDecimal("10.00")); + + LOGGER.info( + "--- Step 5: a new credit limit travels in the event and unblocks the rejected order ---"); + customerService.restart(); + customerService.changeCreditLimit("C-1", new BigDecimal("1500.00")); + logReplica(orderService, "C-1"); + tryToOrder(orderService, "C-1", new BigDecimal("900.00")); } /** diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplica.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplica.java index e6b1ed0c612c..f36890dfa9be 100644 --- a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplica.java +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerReplica.java @@ -24,7 +24,7 @@ */ package com.iluwatar.eventcarriedstatetransfer; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -35,11 +35,13 @@ *

Because each event carries the full state, applying one is a simple upsert. The version * carried by the state guards against events that arrive late or twice: an event is ignored unless * its version is newer than what the replica already holds. + * + *

Not thread-safe; the demo drives it from a single thread. */ @Slf4j public class CustomerReplica { - private final Map customers = new HashMap<>(); + private final Map customers = new LinkedHashMap<>(); /** * Applies an event to the replica. diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerService.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerService.java index 484bd2a8bc4d..b2d3c31f6045 100644 --- a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerService.java +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/CustomerService.java @@ -29,7 +29,6 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; import lombok.extern.slf4j.Slf4j; /** @@ -39,13 +38,15 @@ * {@link CustomerUpdatedEvent} that carries the customer's complete new state. Consumers never need * to query this service to act on the change, which is demonstrated by taking it offline in the * demo while orders keep flowing. + * + *

Not thread-safe; the demo drives it from a single thread. */ @Slf4j public class CustomerService { private final Map customers = new LinkedHashMap<>(); private final EventBus bus; - private final AtomicLong eventSequence = new AtomicLong(); + private long eventSequence; private boolean online = true; /** @@ -118,6 +119,12 @@ public void shutdown() { LOGGER.warn("Customer service is going offline"); } + /** Ends the outage: direct queries are answered again. */ + public void restart() { + online = true; + LOGGER.info("Customer service is back online"); + } + /** Whether direct queries are currently answered. */ public boolean isOnline() { return online; @@ -133,7 +140,7 @@ private CustomerState existing(String customerId) { private CustomerState store(CustomerState state) { customers.put(state.customerId(), state); - var event = new CustomerUpdatedEvent(eventSequence.incrementAndGet(), Instant.now(), state); + var event = new CustomerUpdatedEvent(++eventSequence, Instant.now(), state); LOGGER.info( "Publishing event {} with the full state of {} (version {})", event.eventId(), diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventBus.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventBus.java index 7bf876177a4f..32d534575f65 100644 --- a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventBus.java +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/EventBus.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import lombok.extern.slf4j.Slf4j; /** @@ -45,6 +46,9 @@ public class EventBus { /** * Registers a listener for events of the given class. * + *

Dispatch is by exact runtime class, so the listener receives only events whose class is + * exactly {@code eventType}, never those of a subclass. + * * @param eventType the class of events to receive * @param listener the listener to notify * @param the event type @@ -58,9 +62,14 @@ public void subscribe(Class eventType, EventListener listener) /** * Delivers the event to every listener subscribed to its class. * + *

Only listeners subscribed to the event's exact runtime class are notified; a listener + * subscribed to a supertype of the event does not receive it. + * * @param event the event to publish + * @throws NullPointerException if the event is {@code null} */ public void publish(Object event) { + Objects.requireNonNull(event, "event"); var subscribers = listeners.getOrDefault(event.getClass(), List.of()); if (subscribers.isEmpty()) { LOGGER.warn("No subscribers for {}", event.getClass().getSimpleName()); diff --git a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderService.java b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderService.java index 081ab751514a..a26fa2bb7b34 100644 --- a/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderService.java +++ b/event-carried-state-transfer/src/main/java/com/iluwatar/eventcarriedstatetransfer/OrderService.java @@ -25,7 +25,6 @@ package com.iluwatar.eventcarriedstatetransfer; import java.math.BigDecimal; -import java.util.concurrent.atomic.AtomicLong; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,14 @@ *

The order service subscribes its {@link CustomerReplica} to customer events and afterwards * answers every order using only that replica. It has no reference to the customer service at all, * so it keeps working while the customer service is down and it never adds load to it. + * + *

Not thread-safe; the demo drives it from a single thread. */ @Slf4j public class OrderService { private final CustomerReplica replica = new CustomerReplica(); - private final AtomicLong orderSequence = new AtomicLong(); + private long orderSequence; /** * Creates the service and subscribes its replica to customer events. @@ -74,12 +75,7 @@ public Order placeOrder(String customerId, BigDecimal amount) { + " of " + customerId); } - var order = - new Order( - "ORD-" + orderSequence.incrementAndGet(), - customerId, - customer.shippingAddress(), - amount); + var order = new Order("ORD-" + ++orderSequence, customerId, customer.shippingAddress(), amount); LOGGER.info( "Accepted {} for {} ({}) shipping to '{}' using replica version {}", order.orderId(), diff --git a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerServiceTest.java b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerServiceTest.java index 79060157aa7f..7e317e689a8f 100644 --- a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerServiceTest.java +++ b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/CustomerServiceTest.java @@ -100,4 +100,15 @@ void refusesDirectLookupsWhenOffline() { var thrown = assertThrows(IllegalStateException.class, () -> service.findCustomer("C-1")); assertEquals("customer service is offline", thrown.getMessage()); } + + @Test + void answersDirectLookupsAgainAfterARestart() { + service.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + service.shutdown(); + + service.restart(); + + assertTrue(service.isOnline()); + assertEquals("Alice", service.findCustomer("C-1").orElseThrow().name()); + } } diff --git a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/EventBusTest.java b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/EventBusTest.java index 4d67198c6fae..a2b1f890bd0f 100644 --- a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/EventBusTest.java +++ b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/EventBusTest.java @@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; @@ -62,6 +63,13 @@ void publishingWithoutSubscribersIsHarmless() { assertDoesNotThrow(() -> bus.publish("nobody listens")); } + @Test + void rejectsNullEvents() { + var exception = assertThrows(NullPointerException.class, () -> bus.publish(null)); + + assertEquals("event", exception.getMessage()); + } + @Test void deliversInSubscriptionOrder() { var order = new ArrayList(); diff --git a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/OrderServiceTest.java b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/OrderServiceTest.java index a53eaef0c170..ae29a8901dd7 100644 --- a/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/OrderServiceTest.java +++ b/event-carried-state-transfer/src/test/java/com/iluwatar/eventcarriedstatetransfer/OrderServiceTest.java @@ -91,6 +91,21 @@ void rejectsOrdersAboveTheReplicatedCreditLimit() { assertTrue(thrown.getMessage().contains("exceeds credit limit")); } + @Test + void acceptsOrdersOnceARaisedCreditLimitHasBeenReplicated() { + customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00")); + assertThrows( + OrderRejectedException.class, + () -> orderService.placeOrder("C-1", new BigDecimal("900.00"))); + + customerService.changeCreditLimit("C-1", new BigDecimal("1500.00")); + + var order = orderService.placeOrder("C-1", new BigDecimal("900.00")); + assertEquals(new BigDecimal("900.00"), order.amount()); + assertEquals( + new BigDecimal("1500.00"), orderService.replica().find("C-1").orElseThrow().creditLimit()); + } + @Test void acceptsOrdersExactlyAtTheCreditLimitAndNumbersThemSequentially() { customerService.register("C-1", "Alice", "Lisbon", new BigDecimal("500.00"));