Earlier versions of this chapter published
link.created with the pubsub worker. If your
project still has it, see Migrate from iii-stream and pubsub.link::record_click directly, so the database write runs on
the redirect’s hot path and a slow write slows the redirect. In this chapter you move that work onto
a queue so redirects return immediately, then publish link events on durable topics that
independent subscribers receive: a Python analytics worker and a cache refresher, both decoupled
from the link worker.
Add the workers
This chapter usesqueue and a new Python analytics worker. Uncomment the Ch. 4 block in
worker-compose.yaml:
worker-compose.yaml
analytics database in the Ch. 3 database block. analytics is a second
database: it goes under databases:, at the same indentation as primary:, not inside primary:
or its pool:. The whole block then reads:
worker-compose.yaml
database::query db=analytics later answers UNKNOWN_DB, the analytics entry is nested at
the wrong level.
Make redirects fast with a queue
A queue holds work that is accepted now and run later. Theclicks queue you uncommented above is
the one link::record_click uses.
Queue names are references to the queue and do not place any restrictions on what can be put into
a given queue.
link::record_click in Chapter 3, where http::redirect triggers it directly.
Nothing about that function needs to change to make it queuable. You only change how it’s triggered
with a TriggerAction.
First import TriggerAction into link/src/index.ts:
src/index.ts
action to the existing link::record_click call in http::redirect so the queue
worker enqueues it instead of running it inline:
src/index.ts
link::record_click
drains the queue in the background, with retries and a dead-letter queue if a write keeps failing.
Broadcast events with durable topics
A queue delivers each message to one consumer. When several unrelated parts of the system need to react to the same event, publish it on a topic instead. Thequeue worker serves topics too:
iii::durable::publish sends a message to a topic, and every function bound to that topic with a
durable:subscriber trigger receives its own copy.
Topic messages are durable: each subscriber’s copy is kept until it is handled, retried when the
handler fails, and moved to a dead-letter queue when it keeps failing. Delivery is at least once,
so make subscribers safe to run twice for the same message.For notifications that nobody needs to replay, such as a live click counter, a worker can declare
its own trigger type instead. You’ll build one in Chapter 5.
analytics worker that collects data from these events. We don’t have link updating
functionality yet, so we’ll add that and an HTTP endpoint for it too.
Publish on link.created
Publish an event whenever a link is created. Replacelink::create with this version: it keeps the
creation time in a created_at variable, writes it with the row, and after state::set publishes
it on the link.created topic along with the code and URL. A subscriber then knows when the link
was created, even when its copy arrives later. A taken code throws before the publish, so it
publishes nothing:
src/index.ts
Publish on link.updated
Now add an update path to thelink worker so a link’s target can change, and announce it.
First, the domain function: it updates the database row and publishes a link.updated event on its
own topic, the same way. Place it at the end of link/src/index.ts:
src/index.ts
Expose link updating via HTTP
Then connectlink::update with an HTTP handler that validates input and calls the domain function:
src/index.ts
http::update to PUT /links/:code:
src/index.ts
Add reactive state: Keep the cache correct without coupling
link::update changes the database but not the state cache, so a query could serve stale data.
Rather than handle refreshing the state cache inside link::update, subscribe to the link.updated
topic with a durable:subscriber trigger:
Why durable matters here. This cache refresher must receive every update: a dropped event
would leave the cache pointing at a stale URL. A durable topic keeps the message until the
subscriber handles it, and retries it when the handler fails. Use a durable topic whenever a
missed event would corrupt state.
src/index.ts
Create an analytics worker in Python
Queues and events are useful within a single worker but also between workers. Thus far we’ve been writing all of our code in TypeScript. However workers are not restricted to specific languages or runtimes. So this time we’ll create an analytics worker in Python to count links.Adding a second worker
We already referenced the analytics worker inworker-compose.yaml, now let’s give it some code.
The analytics directory holds a Python worker, with an analytics/src/main.py entrypoint and an
iii.worker.yaml manifest of its own:
analytics/iii.worker.yaml
Subscribe to link.created events
The scaffold shipsanalytics/src/main.py with this code commented out: uncomment its Ch. 4 block
(or replace the file with the code below). It binds a durable:subscriber trigger to the
link.created topic and records every new short link under the UTC day of the event’s
created_at.
Delivery is at least once, so the same event can arrive twice. Instead of adding 1 to a counter,
the worker keeps one row per link in link_events, keyed by code, and writes it with a single
INSERT ... ON CONFLICT(code) DO NOTHING: a redelivered event finds its row and changes nothing.
The per-day counts come from daily_link_counts, a view that counts those rows by day:
analytics/src/main.py
analytics/src/main.py
link worker never has to know it exists.
That is the analytics entry you uncommented at the start of the chapter, alongside the primary
one from Chapter 3.
Restart Compose so it picks up the change:
See it work
Create five links, follow one a few times, and change its target:PUT /links/:code publishes link.updated durably and the subscriber refreshes
the cache, so link::resolve returns the new URL right away:
Conclusion
Redirects no longer wait on a database write: click rows ride a queue, drained in the background. Link events fan out over durable topics to a Python analytics counter and a cache refresher, and thelink worker does not know either of them exists. Next, in
Ch. 5: Stream live clicks, you broadcast every click in real time
from a dedicated click-streamer worker.