Wyobraź sobie, że w twojej aplikacji zachodzą jakieś zmiany (domain eventy). Chcielibyśmy te zmiany wystawić na zewnątrz, żebyśmy mogli na ich podstawie robić sobie raporty, read modele, sagi, synchronizować dane. Czy to zadanie okaże się być trudne czy proste, jeśli użyjemy bazy danych SQL. Co zyskaliśmy dzięki temu, że używam RDBMS/SQL a co utraciliśmy, być może, bezpowrotnie. W tej prezentacji opowiem wam jak chciałem zbudować pewną funkcjonalność dla biblioteki Rails Event Store, dlaczego okazało być się to trudniejsze niż myślałem, o modelu MVCC w PostgreSQL, czy jest sposób, żeby go obejść i uzyskać emulację trybu READ UNCOMMITTED. A może możnaby do całego problemu podejśc zupełnie inaczej i podłączyć się pod Write-Ahead-Log (WAL) i wygrać świat w ten sposób? Pokażę też jak moim zdaniem, korzystając z dokładnie tych samych konceptów, które stoją za Event Sourcingiem i bazami danych moglibyśmy budować API, tak bym za każdym razem pisząc integrację z serwisem X nie musiał się zastanawiać czy jego autorzy rozumieją pojęcie idempotent czy nie. Albo jak moglibyśmy osiągnąć prostotę dzięki używaniu Convergent Replicated Data Types (CRDT). Być może jako community stać nas na więcej niż REST nad CRUDem. Zastanowimy się, czy sprzedawcy SQLa zlasowali nam mózgi, sprawili, że zapomnieliśmy o najprostszym sposobie, który może działać i wprowadzili nas w maliny, w których aktualnie się znajdujemy. A może sami jesteśmy sobie winni? TLDR: Czy nasze aplikacje nie mogłyby działać tak jak pod spodem działają bazy danych? Czy to wszystko musi być takie ciężkie i skomplikowane jeśli chcemy mieć mikro-serwisy, zwłaszcza w małym zespole, który niekoniecznie lubi dostawiać 5 bazę danych do stacku technologicznego.
14. Concurrency level depends on use-cases
Event Sourcing
Optimistic or pessimistic
lock.
1 concurrent write to the
same stream.
Entity state depends on
previous events
Technical Log
Unlimited concurrency
on writes to the same
stream.
Independent events.
14
15. Event Sourcing
ID StreamName Position Type Data
1 order_1 0 OrderPlaced ...
10 order_1 1 OrderShipped ...
23 order_1 2 OrderPaid ...
15
16. Technical Log
ID StreamName Position Type Data
1 Wrocław NULL OrderPlaced ...
10 Wrocław NULL OrderPlaced ...
11 Wrocław NULL OrderPaid ...
16
20. All events = Global Stream
ID StreamName Position Type Data
1 global NULL OrderPlaced ...
2 global NULL OrderShipped ...
3 global NULL OrderPaid ...
20
23. 2 solutions (that I know of) ...
Linearize all writes!
No transactions or short
transactions.
Transactions/Commits
occur one-by-one.
Global, defined order all
events (across all streams)
Workaround
But… how?
23
26. All events = Global Stream (linearized writes)
ID StreamName Position Type Data
1 global 0 OrderPlaced ...
2 global 1 OrderShipped ...
3 global 2 OrderPaid ...
26
32. “
Logical decoding takes the database’s write-ahead
log (WAL), and gives us access to row-level change
events: every time a row in a table is inserted,
updated or deleted, that’s an event.
Those events are grouped by transaction, and
appear in the order in which they were committed
to the database. Aborted/rolled-back transactions
do not appear in the stream.
Thus, if you apply the change events in the same
order, you end up with an exact, transactionally
consistent copy of the database. 32
33. “
The Postgres logical decoding is well designed: it
even creates a consistent snapshot that is
coordinated with the change stream.
You can use this snapshot to make a point-in-time
copy of the entire database (without locking — you
can continue writing to the database while the
copy is being made),
and then use the change stream to get all writes
that happened since the snapshot.
33
35. “ Before you can use logical decoding,
you must set wal_level to logical and
max_replication_slots to at least 1.
35
36. “ The output plugin must be written in C
using the Postgres extension
mechanism, and loaded into the
database server as a shared library.
This requires superuser privileges and
filesystem access on the database
server, so it’s not something to be
undertaken lightly
36
37. “ This is all replication-based !!!
What happens when the client
(replica) stops working?
37
46. 46
SELECT *
FROM event_store_events_in_streams
ORDER BY trans_id, id ASC
WHERE stream = 'global' AND
(
trans_id > last_trans_id
OR (
trans_id = last_trans_id AND
id > last_id
) AND
trans_id < txid_snapshot_xmin(txid_current_snapshot())
47. It works!
i think ;) but…
it waits for longest transaction
even if does not write events
47
50. beyond MVVC in PGSQL
There are a few ways in-progress transactions can
communicate and affect each other:
● Via a shared client application (of course)
● SEQUENCE (and SERIAL) updates happen
immediately, not at commit time
● advisory locking
● Normal row and table locking, but within the
rules of READ COMMITTED visibility
● UNIQUE and EXCLUSION constraints
50
51. 51
SELECT
pg_advisory_lock(0) as getGlobalLock,
nextval('id_seq') as c1,
currval('id_seq') as c2,
pg_advisory_xact_lock(currval('id_seq')) as eid,
setval('id_seq', currval('id_seq') + size-1),
pg_advisory_unlock(0) as releaseGlobalLock,
before inserting events...
55. Solutions summary
◂ linearizing writies
◂ unsuitable for cloud
◂ not working
◂ max delay: longest transaction
◂ max delay: longest transaction which
writes events
55
68. “
MC-Sets resolve divergent histories for an
element by choosing the value which has
changed the most. You cannot delete an
element which is not present, and cannot
add an element which is already present.
MC-sets are compact and do the right
thing when changes to elements are
infrequent compared to the conflict
resolution window, but behave arbitrarily
when divergent histories each include
many changes. 68
69. “
Each element e is associated with an
integer n, implicitly assumed to be zero.
When n is even, the element is absent
from the set. When n is odd, the element
is present. To add an element to the set,
increment n from an even value by one; to
remove an element, increment n from an
odd value by one. To merge sets, take
each element and choose the maximum
value of n from each history.
69