event_engine-delivery 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +22 -39
- data/Rakefile +2 -0
- data/lib/event_engine/delivery/engine.rb +3 -2
- data/lib/event_engine/delivery/processor.rb +30 -0
- data/lib/event_engine/delivery/version.rb +1 -1
- data/lib/event_engine-delivery.rb +1 -1
- data/the_local/agents/event_engine-delivery-develop.md +156 -0
- data/the_local/agents/event_engine-delivery-info.md +69 -0
- data/the_local/agents/event_engine-delivery-install.md +112 -0
- data/the_local/interface.yml +28 -0
- metadata +6 -2
- data/lib/event_engine/delivery/handler.rb +0 -52
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8855fa65e34aece1ee20813207c5092e0aae8cf133fabaed432e61fcb63cd75a
|
|
4
|
+
data.tar.gz: 369f3e3db3efd94a6879b15af7d4c22097814e015d3ee6317ab772cd242efb95
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b52d6548f25680b293362c4980e1244194b32f34fcbee376fc866cc3dc0e61962ad220d2b56465c712f35a47821a93fb358b184fea633532e88f677f30ff9bda
|
|
7
|
+
data.tar.gz: 85cf4fdf4762a80df6cdf465717757148c30e16cb49d3f02fb745fc268581bcf16eee62afb1432c4d70ef860bc2c765b16409b446da987473733262364f4cdae
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ All notable changes to this project are documented here, following
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Delivery registers itself as the processor for the `durable` and `broker` process
|
|
13
|
+
types, in place of a handler registered for every event. An event reaches the
|
|
14
|
+
outbox only when the host's rules route it to `durable` or `broker`.
|
|
15
|
+
- A `durable` event is delivered to its subscribers when no transport is configured.
|
|
16
|
+
|
|
17
|
+
## [0.1.0] - 2026-09-21
|
|
18
|
+
|
|
9
19
|
### Added
|
|
10
20
|
- Initial gem scaffold: mountable `EventEngine::Delivery` Rails engine depending on
|
|
11
21
|
`event_engine`.
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The **delivery layer** for [EventEngine](https://github.com/tylercschneider/even
|
|
|
4
4
|
|
|
5
5
|
`event_engine` (the core) declares events with a schema-first DSL, compiles them to
|
|
6
6
|
a committed schema, and **dispatches** built events to registered handlers. It does
|
|
7
|
-
not deliver anything on its own. `event_engine-delivery` is the
|
|
7
|
+
not deliver anything on its own. `event_engine-delivery` is the processor that takes an
|
|
8
8
|
emitted event and **gets it delivered reliably**:
|
|
9
9
|
|
|
10
10
|
- the durability **level ladder** (1 sync → 2 job → 3 outbox → 4 broker),
|
|
@@ -21,7 +21,7 @@ when you need durable, reliable delivery.
|
|
|
21
21
|
> via copy-then-subtract. Most of its classes still live under the top-level
|
|
22
22
|
> `EventEngine::` namespace (e.g. `EventEngine::Transports::Kafka`,
|
|
23
23
|
> `EventEngine::OutboxEvent`, `EventEngine::OutboxPublisher`). The delivery-specific
|
|
24
|
-
> entry points are `EventEngine::Delivery` (config, engine,
|
|
24
|
+
> entry points are `EventEngine::Delivery` (config, engine, processor). The examples
|
|
25
25
|
> below use the namespaces as they exist today.
|
|
26
26
|
|
|
27
27
|
---
|
|
@@ -87,54 +87,37 @@ end
|
|
|
87
87
|
|
|
88
88
|
## How it hooks into the core
|
|
89
89
|
|
|
90
|
-
At Rails boot, the delivery engine registers
|
|
91
|
-
|
|
90
|
+
At Rails boot, the delivery engine registers itself with the core as the processor
|
|
91
|
+
for the `durable` and `broker` process types:
|
|
92
92
|
|
|
93
93
|
```ruby
|
|
94
94
|
# lib/event_engine/delivery/engine.rb
|
|
95
|
-
initializer "event_engine.delivery.
|
|
95
|
+
initializer "event_engine.delivery.register_processor" do
|
|
96
96
|
config.after_initialize do
|
|
97
|
-
|
|
97
|
+
processor = Processor.new
|
|
98
|
+
Processor::PROCESS_TYPES.each { |process_type| EventEngine.register_processor(process_type, processor) }
|
|
98
99
|
Engine.send(:start_cloud_reporter!)
|
|
99
100
|
end
|
|
100
101
|
end
|
|
101
102
|
```
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
`
|
|
104
|
+
The host's rules file decides which events take those roads, for example
|
|
105
|
+
`quote_signed: durable`. `inline` and `background` events are processed by
|
|
106
|
+
`event_engine-subscribers` and never reach this gem.
|
|
105
107
|
|
|
106
108
|
---
|
|
107
109
|
|
|
108
|
-
##
|
|
110
|
+
## Process type routing
|
|
109
111
|
|
|
110
|
-
`EventEngine::Delivery::
|
|
111
|
-
|
|
112
|
+
`EventEngine::Delivery::Processor#call` writes the event to the outbox, fires
|
|
113
|
+
`event_engine.event_emitted`, and triggers publishing per the configured
|
|
114
|
+
[delivery adapter](#delivery-adapters). When the outbox drains,
|
|
115
|
+
`EventEngine::OutboxRouter` routes by process type:
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
when 2 then dispatch_in_background(event) # enqueue DispatchSubscribersJob
|
|
118
|
-
else write_and_publish(event) # 3, 4, and nil → outbox + publish
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
- **Level 1** — calls every `EventEngine::Subscriber` registered for the event,
|
|
124
|
-
synchronously, in the caller's stack.
|
|
125
|
-
- **Level 2** — enqueues `DispatchSubscribersJob`, which invokes the same subscribers
|
|
126
|
-
in a background job.
|
|
127
|
-
- **Levels 3+ (and `nil`)** — writes an `OutboxEvent` row, fires
|
|
128
|
-
`event_engine.event_emitted`, and triggers publishing per the configured
|
|
129
|
-
[delivery adapter](#delivery-adapters). When the outbox drains,
|
|
130
|
-
`EventEngine::OutboxRouter` differentiates:
|
|
131
|
-
- **level 3** → invoke in-app subscribers,
|
|
132
|
-
- **level 4** → publish to the configured **transport** (raises
|
|
133
|
-
`MissingTransportError` if none/Null),
|
|
134
|
-
- **level 5** → raises `UnsupportedLevelError` (reserved, unsupported).
|
|
135
|
-
|
|
136
|
-
> **`nil` levels fall into the outbox path** (the `else` branch). If you didn't set
|
|
137
|
-
> `event_level` on a definition, its events behave like level 3+. Set it explicitly.
|
|
117
|
+
- **durable** → invokes the event's in-app subscribers. No transport is needed.
|
|
118
|
+
- **broker** → publishes to the configured **transport**, raising
|
|
119
|
+
`MissingTransportError` if none is configured.
|
|
120
|
+
- **sourced** → raises `UnsupportedProcessTypeError`.
|
|
138
121
|
|
|
139
122
|
---
|
|
140
123
|
|
|
@@ -470,9 +453,9 @@ app.
|
|
|
470
453
|
|
|
471
454
|
## Customization recipes
|
|
472
455
|
|
|
473
|
-
**Run delivery and a durable log together.** Add `event_engine-store`.
|
|
474
|
-
|
|
475
|
-
|
|
456
|
+
**Run delivery and a durable log together.** Add `event_engine-store`. Delivery
|
|
457
|
+
processes `durable` and `broker` events, and the store's handlers record every
|
|
458
|
+
event whatever its process type.
|
|
476
459
|
|
|
477
460
|
**Pause delivery during maintenance.** Set `delivery_adapter = :manual`; events keep
|
|
478
461
|
landing in the outbox safely and you drain them with `OutboxPublisher` when ready.
|
data/Rakefile
CHANGED
|
@@ -3,9 +3,10 @@ module EventEngine
|
|
|
3
3
|
class Engine < ::Rails::Engine
|
|
4
4
|
engine_name "event_engine_delivery"
|
|
5
5
|
|
|
6
|
-
initializer "event_engine.delivery.
|
|
6
|
+
initializer "event_engine.delivery.register_processor" do
|
|
7
7
|
config.after_initialize do
|
|
8
|
-
|
|
8
|
+
processor = Processor.new
|
|
9
|
+
Processor::PROCESS_TYPES.each { |process_type| EventEngine.register_processor(process_type, processor) }
|
|
9
10
|
Engine.send(:start_cloud_reporter!)
|
|
10
11
|
end
|
|
11
12
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
module Delivery
|
|
3
|
+
class Processor
|
|
4
|
+
PROCESS_TYPES = %i[durable broker].freeze
|
|
5
|
+
|
|
6
|
+
def call(event)
|
|
7
|
+
outbox_event = OutboxWriter.write(event.to_h)
|
|
8
|
+
|
|
9
|
+
ActiveSupport::Notifications.instrument("event_engine.event_emitted", {
|
|
10
|
+
event_name: outbox_event.event_name,
|
|
11
|
+
event_version: outbox_event.event_version,
|
|
12
|
+
event_id: outbox_event.id,
|
|
13
|
+
idempotency_key: outbox_event.idempotency_key,
|
|
14
|
+
aggregate_type: outbox_event.aggregate_type,
|
|
15
|
+
aggregate_id: outbox_event.aggregate_id,
|
|
16
|
+
aggregate_version: outbox_event.aggregate_version
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
Delivery.enqueue do
|
|
20
|
+
OutboxPublisher.new(
|
|
21
|
+
router: OutboxRouter.new(transport: Delivery.configuration.transport),
|
|
22
|
+
batch_size: Delivery.configuration.batch_size
|
|
23
|
+
).call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
outbox_event
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -16,5 +16,5 @@ require "event_engine/cloud/batch"
|
|
|
16
16
|
require "event_engine/cloud/api_client"
|
|
17
17
|
require "event_engine/cloud/subscribers"
|
|
18
18
|
require "event_engine/cloud/reporter"
|
|
19
|
-
require "event_engine/delivery/
|
|
19
|
+
require "event_engine/delivery/processor"
|
|
20
20
|
require "event_engine/delivery/engine"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-delivery-develop
|
|
3
|
+
description: Use PROACTIVELY for testing durable and broker events with the in-memory transport, scheduling outbox publishing with EventEngine::PublishOutboxEventsJob, scheduling outbox cleanup with EventEngine::OutboxCleanupJob or the cleanup task, and listing or retrying dead-lettered events — MUST BE USED instead of hand-writing test transports, outbox polling jobs, outbox delete queries, or console code that resets failed events.
|
|
4
|
+
tools: Read, Write, Edit, Grep
|
|
5
|
+
scope: delivering durable and broker events — writing them to the transactional outbox, publishing them through a transport, and retrying or dead-lettering the ones that fail
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local writes the host code that tests, schedules and repairs outbox
|
|
9
|
+
delivery. It follows these steps exactly, and where a step names a decision it
|
|
10
|
+
asks the developer and does not pick.
|
|
11
|
+
|
|
12
|
+
## What event_engine-delivery is
|
|
13
|
+
|
|
14
|
+
The delivery layer for EventEngine. Every `durable` and `broker` event is written
|
|
15
|
+
to an outbox table, and a publish run later reads unpublished rows in order,
|
|
16
|
+
hands `durable` events to their in-app subscribers and sends `broker` events
|
|
17
|
+
through the configured transport. A row that fails to publish counts an attempt,
|
|
18
|
+
and a publish run with an attempt limit dead letters it once the limit is
|
|
19
|
+
reached. Use this local once the gem is installed and the work is testing that
|
|
20
|
+
events leave the app, running the publish or cleanup work on a schedule, or
|
|
21
|
+
dealing with events that failed.
|
|
22
|
+
|
|
23
|
+
## Interface
|
|
24
|
+
|
|
25
|
+
- `EventEngine::Transports::InMemoryTransport` — a transport for tests and
|
|
26
|
+
development. `InMemoryTransport.new` builds it, `publish(event)` stores the
|
|
27
|
+
event, and `events` returns every stored outbox record in the order published.
|
|
28
|
+
- `EventEngine::PublishOutboxEventsJob` — an Active Job on the `default` queue
|
|
29
|
+
that takes no arguments and runs one publish run of up to `batch_size`
|
|
30
|
+
unpublished, non-dead-lettered rows with fewer than `max_attempts` attempts. It
|
|
31
|
+
dead letters a row whose attempts reach `max_attempts`, and it raises when the
|
|
32
|
+
configured transport is `nil`.
|
|
33
|
+
- `EventEngine::OutboxCleanupJob` — an Active Job on the `default` queue that
|
|
34
|
+
takes no arguments and deletes published, non-dead-lettered rows published
|
|
35
|
+
more than `retention_period` ago. It does nothing when `retention_period` is
|
|
36
|
+
unset.
|
|
37
|
+
- `bin/rails event_engine:outbox:cleanup` — the same deletion as
|
|
38
|
+
`OutboxCleanupJob`, run once from the shell, printing how many rows it deleted
|
|
39
|
+
or why it deleted none.
|
|
40
|
+
- `bin/rails event_engine:dead_letters:list` — prints every dead-lettered row,
|
|
41
|
+
oldest first, with its id, event name, attempts and the time it was dead
|
|
42
|
+
lettered.
|
|
43
|
+
- `bin/rails event_engine:dead_letters:retry[EVENT_ID]` — resets one
|
|
44
|
+
dead-lettered row to zero attempts with no error and no dead-letter mark, so
|
|
45
|
+
the next publish run picks it up again.
|
|
46
|
+
- `bin/rails event_engine:dead_letters:retry:all` — the same reset for every
|
|
47
|
+
dead-lettered row.
|
|
48
|
+
|
|
49
|
+
## How to use it
|
|
50
|
+
|
|
51
|
+
### Test that events are delivered
|
|
52
|
+
|
|
53
|
+
1. Confirm the event under test is `durable` or `broker`. Other process types
|
|
54
|
+
never reach the outbox. Defining the event and emitting it belong to the
|
|
55
|
+
event_engine-develop local.
|
|
56
|
+
2. In the test setup, keep the current transport and swap in the in-memory one:
|
|
57
|
+
```ruby
|
|
58
|
+
@previous_transport = EventEngine::Delivery.configuration.transport
|
|
59
|
+
@transport = EventEngine::Transports::InMemoryTransport.new
|
|
60
|
+
EventEngine::Delivery.configuration.transport = @transport
|
|
61
|
+
```
|
|
62
|
+
In teardown, put it back, since the configuration is shared by the whole
|
|
63
|
+
process:
|
|
64
|
+
```ruby
|
|
65
|
+
EventEngine::Delivery.configuration.transport = @previous_transport
|
|
66
|
+
```
|
|
67
|
+
3. Emit the event, then run a publish run directly so the assertion does not
|
|
68
|
+
depend on the delivery adapter or on the test's transaction committing:
|
|
69
|
+
```ruby
|
|
70
|
+
EventEngine::PublishOutboxEventsJob.perform_now
|
|
71
|
+
```
|
|
72
|
+
4. For a `broker` event, assert on what reached the transport:
|
|
73
|
+
```ruby
|
|
74
|
+
assert_equal ["order_placed"], @transport.events.map(&:event_name)
|
|
75
|
+
```
|
|
76
|
+
Each entry answers `event_name`, `event_version`, `payload`, `metadata`,
|
|
77
|
+
`idempotency_key` and `occurred_at`.
|
|
78
|
+
5. For a `durable` event, assert on the subscriber's effect instead. `durable`
|
|
79
|
+
events go to in-app subscribers and never reach the transport, so
|
|
80
|
+
`@transport.events` stays empty for them.
|
|
81
|
+
|
|
82
|
+
### Schedule outbox publishing
|
|
83
|
+
|
|
84
|
+
1. Read `delivery_adapter` from the host's delivery initializer.
|
|
85
|
+
- `:manual` — nothing publishes on its own, so `PublishOutboxEventsJob` must
|
|
86
|
+
be scheduled.
|
|
87
|
+
- `:active_job` — each emit already enqueues the job. A schedule is still
|
|
88
|
+
needed for rows that failed, because nothing else re-runs them until the
|
|
89
|
+
next emit.
|
|
90
|
+
- `:inline` — each emit publishes right after its transaction commits, and
|
|
91
|
+
that publish applies no attempt limit, so nothing is ever dead lettered by
|
|
92
|
+
it. A schedule is needed only if the developer wants failed rows dead
|
|
93
|
+
lettered after `max_attempts`.
|
|
94
|
+
Ask the developer whether to add a schedule, and how often it runs.
|
|
95
|
+
2. Ask the developer which scheduler the host uses, for example Solid Queue
|
|
96
|
+
recurring tasks, sidekiq-cron, GoodJob cron or system cron. Look for an
|
|
97
|
+
existing one in the host before asking. This gem ships no scheduler.
|
|
98
|
+
3. Add one recurring entry that enqueues `EventEngine::PublishOutboxEventsJob`
|
|
99
|
+
with no arguments, in that scheduler's own format. For system cron, call it
|
|
100
|
+
through `bin/rails runner "EventEngine::PublishOutboxEventsJob.perform_now"`.
|
|
101
|
+
4. Confirm the configured transport is not `nil`. The job raises when it is.
|
|
102
|
+
|
|
103
|
+
### Schedule outbox cleanup
|
|
104
|
+
|
|
105
|
+
1. Read `retention_period` from the host's delivery initializer. If it is unset, cleanup deletes nothing. Setting it belongs to the
|
|
106
|
+
event_engine-delivery-install local, so ask the developer whether to set one
|
|
107
|
+
there first, and stop here if they decline.
|
|
108
|
+
2. Ask the developer how often cleanup runs.
|
|
109
|
+
3. Add one recurring entry to the host's scheduler that enqueues
|
|
110
|
+
`EventEngine::OutboxCleanupJob` with no arguments. For system cron, run
|
|
111
|
+
`bin/rails event_engine:outbox:cleanup` instead, which also prints what it
|
|
112
|
+
deleted.
|
|
113
|
+
4. To clean up once by hand, run:
|
|
114
|
+
```sh
|
|
115
|
+
bin/rails event_engine:outbox:cleanup
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Handle dead-lettered events
|
|
119
|
+
|
|
120
|
+
1. List them:
|
|
121
|
+
```sh
|
|
122
|
+
bin/rails event_engine:dead_letters:list
|
|
123
|
+
```
|
|
124
|
+
2. Report the list to the developer and ask whether the cause of the failure is
|
|
125
|
+
fixed. A retried row that fails again is dead lettered again after another
|
|
126
|
+
`max_attempts` attempts.
|
|
127
|
+
3. Ask the developer whether to retry one row or all of them.
|
|
128
|
+
- One row, quoting the brackets so the shell does not expand them:
|
|
129
|
+
```sh
|
|
130
|
+
bin/rails "event_engine:dead_letters:retry[42]"
|
|
131
|
+
```
|
|
132
|
+
- All rows:
|
|
133
|
+
```sh
|
|
134
|
+
bin/rails event_engine:dead_letters:retry:all
|
|
135
|
+
```
|
|
136
|
+
4. Retrying does not publish. The row is sent by the next publish run, which is
|
|
137
|
+
the next scheduled `PublishOutboxEventsJob` or the next emitted event under
|
|
138
|
+
the `:inline` or `:active_job` adapter. To send it at once, run:
|
|
139
|
+
```sh
|
|
140
|
+
bin/rails runner "EventEngine::PublishOutboxEventsJob.perform_now"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Conventions
|
|
144
|
+
|
|
145
|
+
- Never write to the outbox table, delete from it, or reset its rows by hand.
|
|
146
|
+
Publishing goes through `PublishOutboxEventsJob`, deletion through
|
|
147
|
+
`OutboxCleanupJob` or the cleanup task, and resets through the retry tasks.
|
|
148
|
+
- Cleanup never deletes an unpublished row or a dead-lettered row.
|
|
149
|
+
- A dead-lettered row stays in the outbox until it is retried.
|
|
150
|
+
- Never write a test transport of your own. Use `InMemoryTransport`, and restore
|
|
151
|
+
the previous transport after each test.
|
|
152
|
+
- Out of scope: adding the gem, the outbox migrations, every delivery
|
|
153
|
+
initializer setting including `retention_period`, and the Kafka transport and
|
|
154
|
+
producer. Those belong to the
|
|
155
|
+
event_engine-delivery-install local. Defining events, choosing their process
|
|
156
|
+
type and emitting them belong to EventEngine itself.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-delivery-info
|
|
3
|
+
description: Use to learn what event_engine-delivery offers — the transactional outbox, durable and broker events, transports, retries, dead letters, and outbox retention.
|
|
4
|
+
tools: Read
|
|
5
|
+
scope: delivering durable and broker events — writing them to the transactional outbox, publishing them through a transport, and retrying or dead-lettering the ones that fail
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local explains event_engine-delivery and makes no changes.
|
|
9
|
+
|
|
10
|
+
## What event_engine-delivery is
|
|
11
|
+
|
|
12
|
+
event_engine-delivery is the delivery layer for EventEngine. EventEngine defines
|
|
13
|
+
and emits events, and each event carries a process type. This gem handles the
|
|
14
|
+
two process types that must not be lost: `durable` and `broker`. It writes each
|
|
15
|
+
such event to an outbox table in the same database transaction as the change
|
|
16
|
+
that caused it. It then publishes the event once that transaction commits.
|
|
17
|
+
|
|
18
|
+
Reach for it when an event has to survive a crash or a rolled-back request, or
|
|
19
|
+
when an event has to leave the app through a message broker. Events with the
|
|
20
|
+
`inline` or `background` process type are not handled here. event_engine-subscribers
|
|
21
|
+
runs those.
|
|
22
|
+
|
|
23
|
+
## Interface
|
|
24
|
+
|
|
25
|
+
This local declares no commands. The other two locals own the whole surface:
|
|
26
|
+
|
|
27
|
+
- **event_engine-delivery-install** owns adding the gem to an app. That covers
|
|
28
|
+
the outbox migrations, the initializer settings, and connecting a real broker
|
|
29
|
+
transport.
|
|
30
|
+
- **event_engine-delivery-develop** owns working with the gem once it is
|
|
31
|
+
installed. That covers the test transport, the jobs that publish and clean up
|
|
32
|
+
the outbox, and the tasks that list and retry dead-lettered events.
|
|
33
|
+
|
|
34
|
+
## How to use it
|
|
35
|
+
|
|
36
|
+
- The gem is not in the app yet, or the app needs a broker connection: use the
|
|
37
|
+
install local.
|
|
38
|
+
- The gem is installed and you are writing tests, scheduling outbox work, or
|
|
39
|
+
handling failed events: use the develop local.
|
|
40
|
+
- You are defining an event or choosing its process type: that belongs to
|
|
41
|
+
EventEngine itself, not this gem.
|
|
42
|
+
|
|
43
|
+
## Conventions
|
|
44
|
+
|
|
45
|
+
- **Outbox** — the table every durable and broker event is written to before it
|
|
46
|
+
is published. A row stays there after publishing until retention removes it.
|
|
47
|
+
- **`durable`** — an event written to the outbox and then handed to its
|
|
48
|
+
subscribers inside the app. It needs no broker.
|
|
49
|
+
- **`broker`** — an event written to the outbox and then published through the
|
|
50
|
+
transport. Publishing raises an error when no real transport is configured.
|
|
51
|
+
- **`sourced`** — a process type for event sourcing. This gem does not support
|
|
52
|
+
it and raises an error when one is routed.
|
|
53
|
+
- **Transport** — the object that sends a broker event out of the app. Any
|
|
54
|
+
object that responds to `publish(event)` qualifies. The default transport
|
|
55
|
+
publishes nothing. A Kafka transport ships with the gem and names topics
|
|
56
|
+
`events.<event_name>`.
|
|
57
|
+
- **Delivery adapter** — when the outbox gets published. `inline` publishes
|
|
58
|
+
right after the transaction commits. `active_job` publishes from a background
|
|
59
|
+
job and requires a real transport. `manual` publishes only when something
|
|
60
|
+
calls for it explicitly.
|
|
61
|
+
- **Batch size** — how many outbox rows one publish run reads. The default is
|
|
62
|
+
100.
|
|
63
|
+
- **Max attempts** — how many failed publishes an event gets before it is dead
|
|
64
|
+
lettered. The default is 5. It applies when the outbox is published by the
|
|
65
|
+
background job. An `inline` publish applies no attempt limit.
|
|
66
|
+
- **Dead letter** — an event that ran out of attempts. It stays in the outbox,
|
|
67
|
+
marked dead lettered, until someone retries it.
|
|
68
|
+
- **Retention period** — how long a published event stays in the outbox. It is
|
|
69
|
+
unset by default, and while it is unset nothing is cleaned up.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-delivery-install
|
|
3
|
+
description: Use to hook event_engine-delivery into a project — copying the outbox migrations, writing the EventEngine::Delivery.configure initializer, and connecting Kafka through EventEngine::Transports::Kafka and EventEngine::KafkaProducer.
|
|
4
|
+
tools: Bash, Read, Edit
|
|
5
|
+
scope: delivering durable and broker events — writing them to the transactional outbox, publishing them through a transport, and retrying or dead-lettering the ones that fail
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local follows these steps exactly and invents none. Where a step names a
|
|
9
|
+
decision, it asks the developer and does not pick.
|
|
10
|
+
|
|
11
|
+
## What event_engine-delivery is
|
|
12
|
+
|
|
13
|
+
The delivery layer for EventEngine: it writes `durable` and `broker` events to an
|
|
14
|
+
outbox table and publishes them after the transaction commits. Hook it in when an
|
|
15
|
+
app emits events that must not be lost or must leave the app through a broker.
|
|
16
|
+
|
|
17
|
+
## Interface
|
|
18
|
+
|
|
19
|
+
- `bin/rails event_engine_delivery:install:migrations` — copies the outbox table
|
|
20
|
+
migrations into the host's `db/migrate/`.
|
|
21
|
+
- `EventEngine::Delivery.configure` — the block in the host initializer that sets
|
|
22
|
+
the delivery adapter, transport, batch size, max attempts, retention period,
|
|
23
|
+
logger and cloud reporting.
|
|
24
|
+
- `EventEngine::Transports::Kafka` — the transport that publishes each broker
|
|
25
|
+
event to the Kafka topic `events.<event_name>`. Built with
|
|
26
|
+
`EventEngine::Transports::Kafka.new(producer: producer)`.
|
|
27
|
+
- `EventEngine::KafkaProducer` — wraps a Kafka client for that transport. Built
|
|
28
|
+
with `EventEngine::KafkaProducer.new(client: client)`. It calls
|
|
29
|
+
`client.produce(json_string, topic: topic)`, so the client must answer that
|
|
30
|
+
call.
|
|
31
|
+
|
|
32
|
+
## How to use it
|
|
33
|
+
|
|
34
|
+
1. Confirm EventEngine itself is installed in the host. If it is not, stop and
|
|
35
|
+
use the event_engine-install local first.
|
|
36
|
+
2. Add the gem to the host `Gemfile` and install it:
|
|
37
|
+
```ruby
|
|
38
|
+
gem "event_engine-delivery"
|
|
39
|
+
```
|
|
40
|
+
```sh
|
|
41
|
+
bundle install
|
|
42
|
+
```
|
|
43
|
+
The gem brings in `event_engine-subscribers` and holds `json` below version 3.
|
|
44
|
+
If the host's lockfile already pins `json` 3 or later, report the conflict to
|
|
45
|
+
the developer and stop.
|
|
46
|
+
3. Copy and run the outbox migrations:
|
|
47
|
+
```sh
|
|
48
|
+
bin/rails event_engine_delivery:install:migrations
|
|
49
|
+
bin/rails db:migrate
|
|
50
|
+
```
|
|
51
|
+
This creates the `event_engine_outbox_events` table and updates
|
|
52
|
+
`db/schema.rb`.
|
|
53
|
+
4. Ask the developer which delivery adapter to use:
|
|
54
|
+
- `:inline` (the default) — publishes right after the transaction commits, in
|
|
55
|
+
the same process.
|
|
56
|
+
- `:active_job` — publishes from a background job, which needs a working
|
|
57
|
+
Active Job backend in the host and a real transport.
|
|
58
|
+
- `:manual` — publishes only when something calls for it explicitly.
|
|
59
|
+
5. Ask the developer whether the app emits `broker` events. If it does, it needs
|
|
60
|
+
a real transport, and Kafka is the one this gem ships. Ask which Kafka client
|
|
61
|
+
the host uses, and confirm that client answers
|
|
62
|
+
`produce(json_string, topic: topic)`. This gem does not add a Kafka client
|
|
63
|
+
gem, so the developer adds and configures the client. If the client does not
|
|
64
|
+
answer that call, report it and stop rather than writing an adapter.
|
|
65
|
+
6. Create `config/initializers/event_engine_delivery.rb`:
|
|
66
|
+
```ruby
|
|
67
|
+
EventEngine::Delivery.configure do |config|
|
|
68
|
+
config.delivery_adapter = :inline
|
|
69
|
+
|
|
70
|
+
# Only when the app emits broker events:
|
|
71
|
+
# producer = EventEngine::KafkaProducer.new(client: kafka_client)
|
|
72
|
+
# config.transport = EventEngine::Transports::Kafka.new(producer: producer)
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
Set `delivery_adapter` to the developer's answer from step 4. Uncomment the
|
|
76
|
+
transport lines when step 5 found broker events, with `kafka_client` replaced
|
|
77
|
+
by the host's client.
|
|
78
|
+
7. Ask the developer for each remaining setting, and write only the ones that
|
|
79
|
+
differ from the default:
|
|
80
|
+
- `config.batch_size` — outbox rows read per publish run. Default `100`.
|
|
81
|
+
- `config.max_attempts` — failed publishes before an event is dead lettered.
|
|
82
|
+
Default `5`.
|
|
83
|
+
- `config.retention_period` — how long published events stay in the outbox,
|
|
84
|
+
for example `30.days`. Default unset, and while unset nothing is removed.
|
|
85
|
+
- `config.logger` — default `Rails.logger`.
|
|
86
|
+
- `config.cloud_api_key`, `config.cloud_environment`, `config.cloud_app_name`
|
|
87
|
+
— setting an API key turns on reporting to EventEngine Cloud at boot. Ask
|
|
88
|
+
before setting it, and read the key from credentials or the environment,
|
|
89
|
+
never a literal.
|
|
90
|
+
8. Boot the app and check the configuration:
|
|
91
|
+
```sh
|
|
92
|
+
bin/rails runner "EventEngine::Delivery.configuration.validate!"
|
|
93
|
+
```
|
|
94
|
+
It raises when the adapter is not one of the three, when `:active_job` has no
|
|
95
|
+
real transport, when the transport does not answer `publish`, or when
|
|
96
|
+
`batch_size` or `max_attempts` is not a positive integer. The app does not run
|
|
97
|
+
this check on its own at boot.
|
|
98
|
+
|
|
99
|
+
## Conventions
|
|
100
|
+
|
|
101
|
+
- The gem registers itself with EventEngine as the processor for `durable` and
|
|
102
|
+
`broker` when the app boots. No routes, mount or processor registration goes in
|
|
103
|
+
the host, and only events the host's rules route to `durable` or `broker` reach
|
|
104
|
+
the outbox.
|
|
105
|
+
- Without a real transport, a `broker` event is still written to the outbox, and
|
|
106
|
+
publishing it raises. `durable` events need no transport.
|
|
107
|
+
- When the gem is upgraded, run `bin/rails event_engine_delivery:install:migrations`
|
|
108
|
+
and `bin/rails db:migrate` again to pick up new outbox migrations.
|
|
109
|
+
- Out of scope: the test transport, scheduling outbox publishing or cleanup, and
|
|
110
|
+
listing or retrying dead-lettered events. Those belong to the
|
|
111
|
+
event_engine-delivery-develop local. Defining events and choosing their process
|
|
112
|
+
type belong to EventEngine itself.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
scope: delivering durable and broker events — writing them to the transactional outbox, publishing them through a transport, and retrying or dead-lettering the ones that fail
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
- bin/rails event_engine_delivery:install:migrations
|
|
5
|
+
- EventEngine::Delivery.configure
|
|
6
|
+
- EventEngine::Transports::Kafka
|
|
7
|
+
- EventEngine::KafkaProducer
|
|
8
|
+
|
|
9
|
+
develop:
|
|
10
|
+
- EventEngine::Transports::InMemoryTransport
|
|
11
|
+
- EventEngine::PublishOutboxEventsJob
|
|
12
|
+
- EventEngine::OutboxCleanupJob
|
|
13
|
+
- bin/rails event_engine:outbox:cleanup
|
|
14
|
+
- bin/rails event_engine:dead_letters:list
|
|
15
|
+
- bin/rails event_engine:dead_letters:retry
|
|
16
|
+
- bin/rails event_engine:dead_letters:retry:all
|
|
17
|
+
|
|
18
|
+
sources:
|
|
19
|
+
- lib/event_engine/delivery.rb
|
|
20
|
+
- lib/event_engine/delivery/configuration.rb
|
|
21
|
+
- lib/event_engine/delivery/handler.rb
|
|
22
|
+
- lib/event_engine/transports/kafka.rb
|
|
23
|
+
- lib/event_engine/kafka_producer.rb
|
|
24
|
+
- lib/event_engine/transports/in_memory_transport.rb
|
|
25
|
+
- app/jobs/event_engine/publish_outbox_events_job.rb
|
|
26
|
+
- app/jobs/event_engine/outbox_cleanup_job.rb
|
|
27
|
+
- lib/tasks/event_engine_outbox.rake
|
|
28
|
+
- lib/tasks/event_engine_dead_letters.rake
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: event_engine-delivery
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -135,7 +135,7 @@ files:
|
|
|
135
135
|
- lib/event_engine/delivery.rb
|
|
136
136
|
- lib/event_engine/delivery/configuration.rb
|
|
137
137
|
- lib/event_engine/delivery/engine.rb
|
|
138
|
-
- lib/event_engine/delivery/
|
|
138
|
+
- lib/event_engine/delivery/processor.rb
|
|
139
139
|
- lib/event_engine/delivery/version.rb
|
|
140
140
|
- lib/event_engine/kafka_producer.rb
|
|
141
141
|
- lib/event_engine/locking_strategy.rb
|
|
@@ -153,6 +153,10 @@ files:
|
|
|
153
153
|
- lib/tasks/event_engine_schema.rake
|
|
154
154
|
- lib/tasks/event_engine_schema_check.rake
|
|
155
155
|
- lib/tasks/event_engine_tasks.rake
|
|
156
|
+
- the_local/agents/event_engine-delivery-develop.md
|
|
157
|
+
- the_local/agents/event_engine-delivery-info.md
|
|
158
|
+
- the_local/agents/event_engine-delivery-install.md
|
|
159
|
+
- the_local/interface.yml
|
|
156
160
|
homepage: https://github.com/tylercschneider/event_engine-delivery
|
|
157
161
|
licenses:
|
|
158
162
|
- MIT
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
module EventEngine
|
|
2
|
-
module Delivery
|
|
3
|
-
# The delivery handler registered with EventEngine core. Receives a built
|
|
4
|
-
# Event and handles only durable/broker events: it writes them to the outbox
|
|
5
|
-
# and publishes them. :inline and :background events are run by
|
|
6
|
-
# event_engine-subscribers, not here.
|
|
7
|
-
class Handler
|
|
8
|
-
def call(event)
|
|
9
|
-
case process_type_for(event)
|
|
10
|
-
when :inline, :background then nil # run by event_engine-subscribers
|
|
11
|
-
else write_and_publish(event)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def process_type_for(event)
|
|
18
|
-
event.process_type&.to_sym
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def write_and_publish(event)
|
|
22
|
-
outbox_event = OutboxWriter.write(event.to_h)
|
|
23
|
-
|
|
24
|
-
ActiveSupport::Notifications.instrument("event_engine.event_emitted", {
|
|
25
|
-
event_name: outbox_event.event_name,
|
|
26
|
-
event_version: outbox_event.event_version,
|
|
27
|
-
event_id: outbox_event.id,
|
|
28
|
-
idempotency_key: outbox_event.idempotency_key,
|
|
29
|
-
aggregate_type: outbox_event.aggregate_type,
|
|
30
|
-
aggregate_id: outbox_event.aggregate_id,
|
|
31
|
-
aggregate_version: outbox_event.aggregate_version
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
Delivery.enqueue do
|
|
35
|
-
transport = Delivery.configuration.transport
|
|
36
|
-
unless transport
|
|
37
|
-
Rails.logger.warn("[EventEngine::Delivery] No transport configured — event written to outbox but not published. " \
|
|
38
|
-
"Set config.transport in your EventEngine::Delivery initializer to enable publishing.")
|
|
39
|
-
next
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
OutboxPublisher.new(
|
|
43
|
-
router: OutboxRouter.new(transport: transport),
|
|
44
|
-
batch_size: Delivery.configuration.batch_size
|
|
45
|
-
).call
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
outbox_event
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|