Skip to content

Commit

Permalink
Add the ability to replay events
Browse files Browse the repository at this point in the history
  • Loading branch information
dadepo committed Aug 27, 2016
1 parent 288374a commit adecd71
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 27 deletions.
152 changes: 128 additions & 24 deletions src/main/java/exploringaxon/AppConfiguration.java
Expand Up @@ -4,15 +4,30 @@
import org.axonframework.commandhandling.SimpleCommandBus; import org.axonframework.commandhandling.SimpleCommandBus;
import org.axonframework.commandhandling.gateway.DefaultCommandGateway; import org.axonframework.commandhandling.gateway.DefaultCommandGateway;
import org.axonframework.contextsupport.spring.AnnotationDriven; import org.axonframework.contextsupport.spring.AnnotationDriven;
import org.axonframework.eventhandling.SimpleEventBus; import org.axonframework.domain.EventMessage;
import org.axonframework.eventhandling.ClassNamePrefixClusterSelector;
import org.axonframework.eventhandling.Cluster;
import org.axonframework.eventhandling.ClusterSelector;
import org.axonframework.eventhandling.ClusteringEventBus;
import org.axonframework.eventhandling.EventBus;
import org.axonframework.eventhandling.EventBusTerminal;
import org.axonframework.eventhandling.SimpleCluster;
import org.axonframework.eventhandling.replay.DiscardingIncomingMessageHandler;
import org.axonframework.eventhandling.replay.IncomingMessageHandler;
import org.axonframework.eventhandling.replay.ReplayingCluster;
import org.axonframework.eventsourcing.EventSourcingRepository; import org.axonframework.eventsourcing.EventSourcingRepository;
import org.axonframework.eventstore.EventStore; import org.axonframework.eventstore.EventStore;
import org.axonframework.eventstore.fs.FileSystemEventStore; import org.axonframework.eventstore.jdbc.JdbcEventStore;
import org.axonframework.eventstore.fs.SimpleEventFileResolver; import org.axonframework.eventstore.management.EventStoreManagement;
import org.axonframework.repository.Repository;
import org.axonframework.unitofwork.NoTransactionManager;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;


import java.io.File; import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;


/** /**
* Created by Dadepo Aderemi. * Created by Dadepo Aderemi.
Expand All @@ -21,54 +36,143 @@
@AnnotationDriven @AnnotationDriven
public class AppConfiguration { public class AppConfiguration {


@Bean
public DataSource dataSource() {
return DataSourceBuilder
.create()
.username("sa")
.password("")
.url("jdbc:h2:mem:exploredb")
.driverClassName("org.h2.Driver")
.build();
}

/**
* An event sourcing implementation needs a place to store events. i.e. The event Store.
* In our use case we will be storing our events in database, so we configure
* the JdbcEventStore as our EventStore implementation
*
* It should be noted that Axon allows storing the events
* in other persistent mechanism...jdbc, jpa, filesystem etc
*
* @return the {@link EventStore}
*/
@Bean
public EventStore jdbcEventStore() {
return new JdbcEventStore(dataSource());
}

@Bean @Bean
public SimpleCommandBus commandBus() { public SimpleCommandBus commandBus() {
SimpleCommandBus simpleCommandBus = new SimpleCommandBus(); SimpleCommandBus simpleCommandBus = new SimpleCommandBus();
return simpleCommandBus; return simpleCommandBus;
} }


/** /**
* The simple command bus, an implementation of an EventBus * A cluster which can be used to "cluster" together event handlers. This implementation is based on
* mostly appropriate in a single JVM, single threaded use case. * {@link SimpleCluster} and it would be used to cluster event handlers that would listen to events thrown
* @return the {@link SimpleEventBus} * normally within the application.
*
* @return an instance of {@link SimpleCluster}
*/ */
@Bean @Bean
public SimpleEventBus eventBus() { public Cluster normalCluster() {
return new SimpleEventBus(); SimpleCluster simpleCluster = new SimpleCluster("simpleCluster");
return simpleCluster;
} }


/**
* A cluster which can be used to "cluster" together event handlers. This implementation is based on
* {@link SimpleCluster} and it would be used to cluster event handlers that would listen to replayed events.
*
* As can be seen, the bean is just a simple implementation of {@link SimpleCluster} there is nothing about
* it that says it would be able to handle replayed events. The bean definition #replayCluster is what makes
* this bean able to handle replayed events.
*
* @return an instance of {@link SimpleCluster}
*/
@Bean @Bean
public DefaultCommandGateway commandGateway() { public Cluster replay() {
return new DefaultCommandGateway(commandBus()); SimpleCluster simpleCluster = new SimpleCluster("replayCluster");
return simpleCluster;
} }


/** /**
* An event sourcing implementation needs a place to store events. i.e. The event Store. * Takes the #replay() cluster and wraps it with a Replaying Cluser, turning the event handlers that are registered
* In our use case we will be storing our events in a file system, so we configure * to be able to pick up events when events are replayed.
* the FileSystemEventStore as our EventStore implementation
* *
* It should be noted that Axon allows storing the events * @return an instance of {@link ReplayingCluster}
* in other persistent mechanism...jdbc, jpa etc */
@Bean
public ReplayingCluster replayCluster() {
IncomingMessageHandler incomingMessageHandler = new DiscardingIncomingMessageHandler();
EventStoreManagement eventStore = (EventStoreManagement) jdbcEventStore();
return new ReplayingCluster(replay(), eventStore, new NoTransactionManager(),0,incomingMessageHandler);
}

/**
* This configuration registers event handlers with the two defined clusters
* *
* @return the {@link EventStore} * @return an instance of {@link ClusterSelector}
*/
@Bean
public ClusterSelector clusterSelector() {
Map<String, Cluster> clusterMap = new HashMap<>();
clusterMap.put("exploringaxon.eventhandler", normalCluster());
clusterMap.put("exploringaxon.replay", replayCluster());
return new ClassNamePrefixClusterSelector(clusterMap);
}


/**
* This replaces the simple event bus that was initially used. The clustering event bus is needed to be able
* to route events to event handlers in the clusters. It is configured with a {@link EventBusTerminal} defined
* by #terminal(). The EventBusTerminal contains the configuration rules which determines which cluster gets an
* incoming event
*
* @return a {@link ClusteringEventBus} implementation of {@link EventBus}
*/ */
@Bean @Bean
public EventStore eventStore() { public EventBus clusteringEventBus() {
EventStore eventStore = new FileSystemEventStore(new SimpleEventFileResolver(new File("./events"))); ClusteringEventBus clusteringEventBus = new ClusteringEventBus(clusterSelector(), terminal());
return eventStore; return clusteringEventBus;
}

/**
* An {@link EventBusTerminal} which publishes application domain events onto the normal cluster
*
* @return an instance of {@link EventBusTerminal}
*/
@Bean
public EventBusTerminal terminal() {
return new EventBusTerminal() {
@Override
public void publish(EventMessage... events) {
normalCluster().publish(events);
}
@Override
public void onClusterCreated(Cluster cluster) {

}
};
}

@Bean
public DefaultCommandGateway commandGateway() {
return new DefaultCommandGateway(commandBus());
} }


/** /**
* Our aggregate root is now created from stream of events and not from a representation in a persistent mechanism, * Our aggregate root is now created from stream of events and not from a representation in a persistent mechanism,
* thus we need a repository that can handle the retrieving of our aggregate root from the stream of events. * thus we need a repository that can handle the retrieving of our aggregate root from the stream of events.
* *
* We configure the EventSourcingRepository which does exactly this. We supply it with the event store * We configure the EventSourcingRepository which does exactly this. We supply it with the event store
* @return {@link EventSourcingRepository} * @return a {@link EventSourcingRepository} implementation of {@link Repository}
*/ */
@Bean @Bean
public EventSourcingRepository eventSourcingRepository() { public Repository<Account> eventSourcingRepository() {
EventSourcingRepository eventSourcingRepository = new EventSourcingRepository(Account.class, eventStore()); EventSourcingRepository eventSourcingRepository = new EventSourcingRepository(Account.class, jdbcEventStore());
eventSourcingRepository.setEventBus(eventBus()); eventSourcingRepository.setEventBus(clusteringEventBus());
return eventSourcingRepository; return eventSourcingRepository;
} }
} }
10 changes: 10 additions & 0 deletions src/main/java/exploringaxon/api/event/AccountCreditedEvent.java
@@ -1,5 +1,8 @@
package exploringaxon.api.event; package exploringaxon.api.event;


import java.time.LocalDateTime;
import java.time.ZoneId;

/** /**
* Event Class that communicates that an account has been credited * Event Class that communicates that an account has been credited
* *
Expand All @@ -10,11 +13,14 @@ public class AccountCreditedEvent {
private final String accountNo; private final String accountNo;
private final Double amountCredited; private final Double amountCredited;
private final Double balance; private final Double balance;
private final long timeStamp;


public AccountCreditedEvent(String accountNo, Double amountCredited, Double balance) { public AccountCreditedEvent(String accountNo, Double amountCredited, Double balance) {
this.accountNo = accountNo; this.accountNo = accountNo;
this.amountCredited = amountCredited; this.amountCredited = amountCredited;
this.balance = balance; this.balance = balance;
ZoneId zoneId = ZoneId.systemDefault();
this.timeStamp = LocalDateTime.now().atZone(zoneId).toEpochSecond();
} }


public String getAccountNo() { public String getAccountNo() {
Expand All @@ -28,4 +34,8 @@ public Double getAmountCredited() {
public Double getBalance() { public Double getBalance() {
return balance; return balance;
} }

public long getTimeStamp() {
return timeStamp;
}
} }
10 changes: 10 additions & 0 deletions src/main/java/exploringaxon/api/event/AccountDebitedEvent.java
@@ -1,5 +1,8 @@
package exploringaxon.api.event; package exploringaxon.api.event;


import java.time.LocalDateTime;
import java.time.ZoneId;

/** /**
* Event Class that communicates that an account has been debited * Event Class that communicates that an account has been debited
* *
Expand All @@ -9,11 +12,14 @@ public class AccountDebitedEvent {
private final String accountNo; private final String accountNo;
private final Double amountDebited; private final Double amountDebited;
private final Double balance; private final Double balance;
private final long timeStamp;


public AccountDebitedEvent(String accountNo, Double amountDebited, Double balance) { public AccountDebitedEvent(String accountNo, Double amountDebited, Double balance) {
this.accountNo = accountNo; this.accountNo = accountNo;
this.amountDebited = amountDebited; this.amountDebited = amountDebited;
this.balance = balance; this.balance = balance;
ZoneId zoneId = ZoneId.systemDefault();
this.timeStamp = LocalDateTime.now().atZone(zoneId).toEpochSecond();
} }


public String getAccountNo() { public String getAccountNo() {
Expand All @@ -27,4 +33,8 @@ public Double getAmountDebited() {
public Double getBalance() { public Double getBalance() {
return balance; return balance;
} }

public long getTimeStamp() {
return timeStamp;
}
} }
@@ -0,0 +1,51 @@
package exploringaxon.replay;

import exploringaxon.api.event.AccountCreditedEvent;
import exploringaxon.api.event.AccountDebitedEvent;
import org.axonframework.eventhandling.annotation.EventHandler;
import org.axonframework.eventhandling.replay.ReplayAware;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

@Component
public class AccountCreditedReplayEventHandler implements ReplayAware {

List<String> audit = new ArrayList<>();

@EventHandler
public void handle(AccountCreditedEvent event) {
String auditMsg = String.format("%s credited to account with account no {%s} on %s",
event.getAmountCredited(), event.getAccountNo(), formatTimestampToString(event.getTimeStamp()));
audit.add(auditMsg);
}

@EventHandler
public void handle(AccountDebitedEvent event) {
String auditMsg = String.format("%s debited from account with account no {%s} on %s",
event.getAmountDebited(), event.getAccountNo(), formatTimestampToString(event.getTimeStamp()));
audit.add(auditMsg);
}

public List<String> getAudit() {
return audit;
}

@Override
public void beforeReplay() {
audit.clear();
}

@Override
public void afterReplay() {
}

@Override
public void onReplayFailed(Throwable cause) {}

private String formatTimestampToString(long timestamp) {
return new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(timestamp * 1000);
}
}
23 changes: 23 additions & 0 deletions src/main/java/exploringaxon/web/IndexController.java
Expand Up @@ -2,8 +2,11 @@


import exploringaxon.api.command.CreditAccountCommand; import exploringaxon.api.command.CreditAccountCommand;
import exploringaxon.api.command.DebitAccountCommand; import exploringaxon.api.command.DebitAccountCommand;
import exploringaxon.replay.AccountCreditedReplayEventHandler;
import org.axonframework.commandhandling.gateway.CommandGateway; import org.axonframework.commandhandling.gateway.CommandGateway;
import org.axonframework.eventhandling.replay.ReplayingCluster;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model; import org.springframework.ui.Model;
Expand All @@ -17,6 +20,14 @@
@Controller @Controller
public class IndexController { public class IndexController {


@Autowired
@Qualifier("replayCluster")
ReplayingCluster replayCluster;

@Autowired
AccountCreditedReplayEventHandler replayEventHandler;


@Autowired @Autowired
private CommandGateway commandGateway; private CommandGateway commandGateway;


Expand All @@ -26,6 +37,11 @@ public String index(Model model) {
return "index"; return "index";
} }


@RequestMapping("/about")
public String about() {
return "about";
}



@RequestMapping("/debit") @RequestMapping("/debit")
@Transactional @Transactional
Expand All @@ -42,4 +58,11 @@ public void doCredit(@RequestParam("acc") String accountNumber, @RequestParam("a
CreditAccountCommand creditAccountCommandCommand = new CreditAccountCommand(accountNumber, amount); CreditAccountCommand creditAccountCommandCommand = new CreditAccountCommand(accountNumber, amount);
commandGateway.send(creditAccountCommandCommand); commandGateway.send(creditAccountCommandCommand);
} }

@RequestMapping("/events")
public String doReplay(Model model) {
replayCluster.startReplay();
model.addAttribute("events",replayEventHandler.getAudit());
return "events";
}
} }
28 changes: 28 additions & 0 deletions src/main/resources/schema.sql
@@ -0,0 +1,28 @@
CREATE TABLE domainevententry
(
aggregateidentifier VARCHAR(255) NOT NULL,
sequencenumber BIGINT NOT NULL,
type VARCHAR(255) NOT NULL,
eventidentifier VARCHAR(255) NOT NULL,
metadata BYTEA,
payload BYTEA,
payloadrevision VARCHAR(255),
payloadtype VARCHAR(255) NOT NULL,
timestamp VARCHAR(255) NOT NULL,
CONSTRAINT newdomainevententry_pkey PRIMARY KEY (aggregateidentifier, sequencenumber, type)
);


CREATE TABLE snapshotevententry
(
aggregateidentifier VARCHAR(255) NOT NULL,
sequencenumber BIGINT NOT NULL,
type VARCHAR(255) NOT NULL,
eventidentifier VARCHAR(255) NOT NULL,
payloadrevision VARCHAR(255),
payloadtype VARCHAR(255) NOT NULL,
timestamp VARCHAR(255) NOT NULL,
metadata BYTEA,
payload BYTEA NOT NULL,
CONSTRAINT snapshotevententry_pkey1 PRIMARY KEY (aggregateidentifier, sequencenumber, type)
);

0 comments on commit adecd71

Please sign in to comment.