Current section

Files

Jump to
eventstore lib event_store subscriptions persistent_subscription.ex
Raw

lib/event_store/subscriptions/persistent_subscription.ex

defmodule EventStore.Subscriptions.PersistentSubscription do
require Logger
defmodule SubscriptionData do
defstruct stream_uuid: nil,
subscription_name: nil,
subscriber: nil,
last_seen_event_id: 0,
latest_event_id: 0
end
alias EventStore.Storage
alias EventStore.Subscriptions.PersistentSubscription
use Fsm, initial_state: :initial, initial_data: %SubscriptionData{}
@all_stream "$all"
defstate initial do
defevent subscribe(stream_uuid, subscription_name, subscriber), data: %SubscriptionData{} = data do
case subscribe_to_stream(stream_uuid, subscription_name) do
{:ok, subscription} ->
data = %SubscriptionData{data |
stream_uuid: stream_uuid,
subscription_name: subscription_name,
subscriber: subscriber,
last_seen_event_id: subscription.last_seen_event_id
}
next_state(:catching_up, data)
{:error, reason} ->
next_state(:failed, data)
end
end
end
defstate catching_up do
defevent catch_up, data: %SubscriptionData{last_seen_event_id: last_seen_event_id} = data do
case query_latest_event_id do
0 ->
# no published events
next_state(:subscribed, data)
^last_seen_event_id ->
# already seen latest published event
next_state(:subscribed, data)
latest_event_id ->
# must catch-up with all unseen events
data = catch_up_to_event(data, latest_event_id)
next_state(:subscribed, data)
end
end
# ignore event notifications while catching up; but remember the latest event id
defevent notify_events(events, latest_event_id), data: %SubscriptionData{} = data do
data = %SubscriptionData{data | latest_event_id: latest_event_id}
next_state(:catching_up, data)
end
end
defstate subscribed do
defevent notify_events(events), data: %SubscriptionData{} = data do
# TODO: move to catching-up state if the event id of the first event is not `data.last_seen_event_id + 1`
latest_event_id = List.last(events).event_id
notify_subscriber(data, events)
ack_events(data, events)
data = %SubscriptionData{data |
latest_event_id: latest_event_id,
last_seen_event_id: latest_event_id
}
next_state(:subscribed, data)
end
defevent unsubscribe, data: %SubscriptionData{} = data do
next_state(:unsubscribed, data)
end
end
defstate unsubscribed do
end
defp subscribe_to_stream(stream_uuid, subscription_name) do
Storage.subscribe_to_stream(stream_uuid, subscription_name)
end
defp query_latest_event_id do
{:ok, latest_event_id} = Storage.latest_event_id
latest_event_id
end
defp catch_up_to_event(%SubscriptionData{stream_uuid: stream_uuid, last_seen_event_id: last_seen_event_id} = data, latest_event_id) do
case unseen_events(data) do
{:ok, events} ->
# chunk events by stream
events
|> Enum.chunk_by(fn event -> event.stream_id end)
|> Enum.each(fn events_by_stream ->
notify_subscriber(data, events_by_stream)
ack_events(data, events_by_stream)
end)
end
data = %SubscriptionData{data |
latest_event_id: latest_event_id,
last_seen_event_id: latest_event_id
}
end
defp unseen_events(%SubscriptionData{stream_uuid: @all_stream, last_seen_event_id: last_seen_event_id}) do
start_event_id = last_seen_event_id + 1
Storage.read_all_streams_forward(start_event_id)
end
defp unseen_events(%SubscriptionData{stream_uuid: stream_uuid, last_seen_event_id: last_seen_event_id}) do
start_version = last_seen_event_id + 1
Storage.read_stream_forward(stream_uuid, start_version)
end
defp notify_subscriber(%SubscriptionData{subscriber: subscriber} = data, events) do
send(subscriber, {:events, events})
end
defp ack_events(%SubscriptionData{subscription_name: subscription_name} = data, events) do
# TODO
# latest_event_id = List.last(events).event_id
#
# Storage.ack_last_seen_event(stream_id, subscription_name, latest_event_id)
end
end