Not sure yet.
Just some tinkering as I try to learn about event based systems- I'm trying to head towards CQRS but I also wanna develop a generalised pub/sub event framework in the process.
- Domain
- A slice of functionality ideally centered around something real-world (e.g.
wallet
)
- A slice of functionality ideally centered around something real-world (e.g.
- Entity
- An instance of something within a domain (e.g.
wallet.28sshuU4BSZ2RCJyTHt2CS5yVeQ
)
- An instance of something within a domain (e.g.
- Server
- The user-facing HTTP entrypoint into a domain (e.g.
http://wallet_server/wallet/28sshuU4BSZ2RCJyTHt2CS5yVeQ/balance
)
- The user-facing HTTP entrypoint into a domain (e.g.
- Writer
- The write path for an entity in a domain (e.g.
nats://message_broker:4222/event.wallet.28sshuU4BSZ2RCJyTHt2CS5yVeQ.credit
)
- The write path for an entity in a domain (e.g.
- Reader
- The read path for an entity in a domain (e.g.
redis://cache:6379/event.wallet.28sshuU4BSZ2RCJyTHt2CS5yVeQ.balance
)
- The read path for an entity in a domain (e.g.
- Find out why SQLite falls over (and doesn't recover) when you pummel the writer
- Okay, it's because I had the
sqlite3
shell loops running- need to work out how to get the server to recover though (basically one instance of the database being busy locks it for good until manually recovered)
- Okay, it's because I had the
- Add /healthz endpoint for all services
- Go 1.21+
- Docker
- Docker Compose
- Redis CLI
- cURL
- jq
And optionally for the utilities / integration tests:
- Python3.9+
- Virtualenv
./pull.sh && ./build.sh
Foreground
./run.sh
Background
./run_in_background.sh
Assuming you've got everything up and running, open a bunch of shells as follows...
while true; do clear; redis-cli GET wallet.28skwt5B8zTrs6AqBWrSgCHLcRL | jq; sleep 1; done
while true; do clear; ./docker-compose.sh exec wallet_writer_service sqlite3 /var/lib/sqlite/data/datastore.db -line 'SELECT * FROM event;'; sleep 1; done
while true; do clear; ./docker-compose.sh exec wallet_writer_service sqlite3 /var/lib/sqlite/data/datastore.db -line 'SELECT * FROM state;'; sleep 1; done
while true; do clear; curl -s http://localhost/wallet/28skwt5B8zTrs6AqBWrSgCHLcRL/balance | jq; sleep 1; done
while true; do clear; curl -s http://localhost/wallet/28skwt5B8zTrs6AqBWrSgCHLcRL/transactions | jq; sleep 1; done
A debit attempt should fail due to zero balance
curl -s -X POST -d '{"amount": 5}' http://localhost/wallet/28skwt5B8zTrs6AqBWrSgCHLcRL/debit | jq
A credit attempt should increase the balance
curl -s -X POST -d '{"amount": 5}' http://localhost/wallet/28skwt5B8zTrs6AqBWrSgCHLcRL/credit | jq
And now the same debit attempt should succeed
curl -s -X POST -d '{"amount": 5}' http://localhost/wallet/28skwt5B8zTrs6AqBWrSgCHLcRL/debit | jq
Observations in the other shell windows:
- Redis is updated with state for readers to use
- Event log contains all attempted transactions
- State log contains the state after each transaction
- Balance updates appropriately
- Transactions update appropriately
If you really wanna pummel the system, set yourself up with a Virtualenv, install requirements.txt
and try the following:
# to smash the reads
python -m utils.curl -s --loop --workers 64 --period 0 http://localhost/wallet/28skwt5B8zTrs6AqBWrSgCHLcRL/balance
# to smash the writes
python -m utils.curl --loop --workers 16 --period 0 -s -X POST -d '{"amount": 5}' http://localhost/wallet/28skwt5B8zTrs6AqBWrSgCHLcRL/credit
NOTE: If you've still got shells running making sqlite3
calls you'll like lock your database- cancel those first.
This will spin up 64 threads all requesting the balance as fast as they can- I get maybe 250 to 350 requests per second on my Macbook Pro and as far as I can tell, the system is the limiting factor- attempts to add more workers or more entire instances of that command see requests per second reduced.
Not sure where the bottleneck is- no single container is working particularly hard, maybe it's just Docker for Mac things.
message_broker
= NATS for pub/sub gluecache
= Redis for caching read statehistory_writer_datastore
= SQLite for storing global event logshistory_writer_service
= Go code to record global write eventswallet_writer_datastore
= SQLite for storing event logs and state logswallet_writer_service
= Go code to handle write eventswallet_server_service
= Go code to expose read state
wallet_server_service
is really just a convenience abstraction to expose the reader and writer via HTTP- All reads ultimately happen against Redis
- All writes are handled by
wallet_writer_service
, which ensures to:- Record write events in the event log
- Interact with the write model
- Write the full state to the read model (Redis)
wallet_server_service
handleshttp://localhost/wallet/28skwt5B8zTrs6AqBWrSgCHLcRL/balance
- The
Reader
abstraction hasGetBalance
called GetBalance
extractsBalance
fromGetWalletState
GetWalletState
invokesGetState
GetState
attempts to get JSON from Redis- NOTE: We leave the
wallet_server_service
process by interacting with Redis
- NOTE: We leave the
- Data flows back up and out to the requester (if available)
wallet_server_service
handleshttp://localhost/wallet/28skwt5B8zTrs6AqBWrSgCHLcRL/credit
- The
Caller
abstraction hasCredit
called Credit
invokesCall
Call
creates an event and attempts a NATSRequest
(RPC) with it- NOTE: We leave the
wallet_server_service
process by interacting with NATS
- NOTE: We leave the
wallet_writer_service
handles the event in the NATSRequest
with theWriter
abstraction- The
Writer
abstraction usehandle
to record the event in the event log and pass it up to the domain implementation - The domain implementation invokes the appropriate method against the
Wallet
abstraction - The
Wallet
abstraction accepts or rejects the method call, possibly mutating it's state - The domain implementation extracts the
Wallet
abstractions state and updates Redis with it- NOTE: At this point, a reader will see the state affected by the recently written event