You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: frontend/docs/pages/cookbooks/workflow-aws-s3.mdx
+54-26Lines changed: 54 additions & 26 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,16 +7,29 @@ import FanoutDiagram from "@/components/FanoutDiagram";
7
7
8
8
# Processing Amazon S3 Objects at Scale
9
9
10
-
Amazon S3 provides a robust, predictable way to store and retrieve large blobs of data. It has several properties that make it an ideal backend for processing massive datasets at scale. Namely, it provides strong [read-after-write consistency](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html#ConsistencyModel)for `PUT` and `DELETE` requests. This guarantees that when we retrieve or modify data within a bucket, we are always working with the most up-to-date state.
10
+
Amazon S3 is a reliable, boring distributed object store with strong [read-after-write consistency](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html#ConsistencyModel)guarantees, which makes it an ideal backend for processing massive datasets at scale. Coordinating workloads that actually leverage that scale, while not running up a comically large cloud bill, is non-trivial.
11
11
12
-
Moreover, S3's data consistency model pairs nicely with Hatchet's [concurrency control](/v1/concurrency) features and dynamic spawning of [child tasks in parallel](/v1/child-spawning#fan-out-spawning-many-children-in-parallel), allowing us to aggressively poll and process S3 objects while sidestepping data races.
12
+
This guide walks through building an _embarrassingly parallel_ Hatchet pipeline for ingesting and processing hundreds of thousands of images stored across Amazon S3. We'll be making extensive use of Hatchet's [concurrency control](/v1/concurrency) features, paired with aggressive fan-outs via [dynamic child spawning](/v1/child-spawning#fan-out-spawning-many-children-in-parallel), to parallelize the heck out of our data processing workflow while ensuring fair allocation of resources and idempotent processing of objects.
13
13
14
-
This example illustrates how to build an _embarrassingly parallel_ Hatchet pipeline for ingesting and processing hundreds of thousands of images stored across Amazon S3. The constraints for the system are as follows:
Without concurrency control, nothing stops multiple workers all grabbing, and processing, the same object.
22
+
</figcaption>
23
+
</figure>
24
+
25
+
### Scenario
26
+
27
+
For the purposes of this guide, we'll constrain the system as follows:
15
28
16
29
- Each bucket corresponds to a separate class of objects.
17
30
- Bucket sizes are unevenly distributed.
18
-
- Objects are ephemeral. Once successfully processed, the object should be deleted.
19
-
-More data can be added or removed dynamically at any point.
31
+
- Objects are ephemeral. Once successfully processed, an object should be deleted.
32
+
-Data can be added or removed at any point, with a sizable chunk being front-loaded at startup.
20
33
21
34
## Setup
22
35
@@ -44,6 +57,10 @@ _Note: `<your-bucket-prefix>` should be scoped at runtime to the specific bucket
44
57
</Tabs.Tab>
45
58
</UniversalTabs>
46
59
60
+
<Callouttype="info">
61
+
For the purposes of this example, we recommend using `localstack/localstack:4.14`.
62
+
</Callout>
63
+
47
64
</Steps>
48
65
49
66
### Define the Models
@@ -63,16 +80,14 @@ Notice that we only have models defined for our input data, as the task naturall
63
80
64
81
## Workflows
65
82
66
-
To ensure maximum parallelism, we treat the processing of each object within each bucket as an independent unit of work.
67
-
68
-
The execution sequence is:
83
+
To ensure maximum parallelism, we treat the processing of each object within each bucket as an independent unit of work. Concretely, the execution sequence is:
69
84
70
85
1. Fetch buckets from S3.
71
86
2. Fetch objects within each bucket.
72
87
3. Process each object.
73
88
4. Delete each object.
74
89
75
-
We use a double fan-out pattern here to ensure that we can distribute the list operations and the processing operations across a horizontally scaled fleet of workers without hitting bottlenecks.
90
+
We implement this as a _double fan-out_: one fan-out across buckets to spawn per-bucket polling tasks, and a second fan-out within each polling task to spawn one processing task per object. This lets us distribute both the listing and processing work across a horizontally scaled fleet of workers without any single worker becoming a bottleneck.
76
91
77
92
<FanoutDiagram
78
93
parentLabel="S3::ListBuckets"
@@ -97,9 +112,9 @@ We use a double fan-out pattern here to ensure that we can distribute the list o
97
112
98
113
We start by fetching all relevant buckets from S3. For simplicity, this pipeline forgoes [S3 Event Notifications](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html) in favor of periodic scanning via a [cron trigger](/v1/cron-runs).
99
114
100
-
As the root of the pipeline, this task iterates through buckets serially and fans out a child workflow per bucket. Since it produces input for downstream parallelism rather than executing parallel work itself, concurrent execution offers no benefit at this stage.
115
+
As the root of the pipeline, this task paginates across all buckets in our region, fanning out a child workflow per bucket. Since we're just iterating across mini-batches/pages of buckets, there's no meaningful work here to parallelize.
101
116
102
-
We must guard against overlapping executions. If the task is throttled by the S3 API and the cron fires again before it completes, newer runs should be rejected until the in-flight run finishes.
117
+
However, our strategy should still guard against _overlapping executions_ of the same cron job. If S3 throttles us mid-scan and a run takes longer than the cron interval, the next tick should be rejected until the in-flight run finishes, rather than piling on more concurrent listing calls and making the throttling worse.
103
118
104
119
<FanoutDiagram
105
120
parentLabel="Fetch Buckets"
@@ -119,9 +134,9 @@ Taking all of this into account, we define our `fetch_s3_buckets` workflow as fo
119
134
</Tabs.Tab>
120
135
</UniversalTabs>
121
136
122
-
Here, we set the concurrency key to the CEL expression `'singleton'`, a string literal that evaluates to the same value for every run. Combined with `max_runs=1` and `CANCEL_NEWEST`, this rule funnels all runs through **a single slot on a single worker**. While one is in-flight, any new run triggered by the cron is immediately canceled.
137
+
Here, we set the concurrency key to the CEL expression `'singleton'`, a string literal that evaluates to the same value for every run. Combined with `max_runs=1` and `CANCEL_NEWEST`, this rule funnels all runs through **a single slot on a single worker**: while one is in-flight, any new run triggered by the cron is immediately canceled.
123
138
124
-
Mapping to our implementation, the `fetch_s3_buckets` workflow is responsible for gathering all relevant buckets from our region and spawning a new `fetch_objects` task from the `fetch_s3_objects` workflow for each.
139
+
The task body itself is responsible for gathering all relevant buckets from our region and spawning a new `fetch_objects` task from the `fetch_s3_objects` workflow for each:
125
140
126
141
<UniversalTabsitems={["Typescript", "Python"]}>
127
142
<Tabs.Tabtitle="Typescript">
@@ -132,7 +147,7 @@ Mapping to our implementation, the `fetch_s3_buckets` workflow is responsible fo
132
147
</Tabs.Tab>
133
148
</UniversalTabs>
134
149
135
-
For a given trigger, we use the bucket name as a `child_key`to ensure deduplication.
150
+
We use the bucket name as its child key for each spawn to ensure deduplication of downstream `fetch_objects` runs.
136
151
137
152
### Fetch Objects
138
153
@@ -149,7 +164,7 @@ Next, we define a workflow responsible for polling a given S3 bucket for objects
149
164
showCollect={false}
150
165
/>
151
166
152
-
Similarly to [Fetch Buckets](#fetch-buckets-workflow), this workflow needs to ensure each bucket has at most one poller in-flight at a time, preventing overlaps via the `CANCEL_NEWEST` strategy and `max_runs=1`. Here, however, we group by bucket name using the CEL expression `input.bucket` to enforce per-bucket concurrency.
167
+
Similarly to [Fetch Buckets](#fetch-buckets-workflow), this workflow needs to ensure each bucket has at most one poller in-flight at a time, preventing overlaps via the `CANCEL_NEWEST` strategy and constraining to a single run. Here, however, we group by bucket name using the CEL expression `input.bucket` to enforce per-bucket concurrency.
153
168
154
169
Additionally, we define a global concurrency expression that limits how many bucket pollers can spin up simultaneously. Tasks exceeding this rule are enqueued using the `GROUP_ROUND_ROBIN` strategy and will execute when concurrent slots free up.
155
170
@@ -175,20 +190,24 @@ The task itself paginates through the bucket's objects and spawns a processing r
175
190
</Tabs.Tab>
176
191
</UniversalTabs>
177
192
178
-
The `child_key` here combines the bucket name and object key `({bucket}/{key})`, guaranteeing that re-triggering the parent won't spawn duplicate processing runs for the same object. S3 object keys are only unique within a specific bucket, so the bucket prefix is mandatory here.
193
+
The child key here combines the bucket name and object key `({bucket}/{key})`, guaranteeing that re-triggering the parent won't spawn duplicate processing runs for the same object. S3 object keys are only unique within a specific bucket, so the bucket prefix is mandatory here.
179
194
180
195
### Process Objects
181
196
182
-
<AmazonS3ImagePipelineDiagram />
183
197
184
-
Finally, we define the processing workflow, which represents the bulk of the pipeline's actual computational work. Recall from the previous sections that:
198
+
Lastly, we define the processing workflow, which represents the bulk of the pipeline's actual computational work. Recall from the previous sections that:
185
199
186
200
1. The distribution of objects per bucket is not uniform.
187
201
2. Processed objects are immediately deleted.
188
202
203
+
189
204
Rather than capping concurrency globally, we scope the limit _per bucket_ using `S3_WORKER_MAX_RUNS_PER_BUCKET`. A global cap would allow the largest buckets to monopolize all processing slots. A per-bucket cap keeps throughput fair across the system regardless of individual bucket size. When a bucket hits its limit, additional runs are enqueued and dispatched round-robin via `GROUP_ROUND_ROBIN` as slots open.
190
205
191
-
There is no child-side deduplication on this workflow (no concurrency expression keyed on `{bucket}/{object-key}`). Because `process-object` runs are only ever spawned by [`fetch_objects`](#fetch-objects-workflow)—which already handles deduplication via `child_key` at spawn time—a secondary concurrency expression would be redundant.
206
+
<AmazonS3ImagePipelineDiagram/>
207
+
208
+
The diagram above shows this steady state: work interleaves across buckets, the worker pool runs at partial capacity, and a backlog drains in round-robin order. Even when `bucket-1` has the most pending objects, it doesn't monopolize the workers.
209
+
210
+
There is no child-side deduplication on this workflow (no concurrency expression keyed on `{bucket}/{object-key}`). Because `process-object` runs are only ever spawned by [`fetch_objects`](#fetch-objects-workflow) (which already handles deduplication via child key at spawn time) a secondary concurrency expression would be redundant.
192
211
193
212
<UniversalTabsitems={["Typescript", "Python"]}>
194
213
<Tabs.Tabtitle="Typescript">
@@ -223,13 +242,13 @@ This architecture provides an idempotent, effectively-exactly-once processing pi
223
242
224
243
## Test it
225
244
226
-
The test suite manages LocalStack via [Docker Compose](https://docs.docker.com/compose/). The start and teardown are fully automatic.
227
-
228
-
Each test seeds 3 buckets with 3 synthetic PNG objects each, triggers `fetch_s3_buckets` manually, and polls S3 until every object has been deleted by the pipeline, validating the full fan-out chain end-to-end.
229
-
230
245
<UniversalTabsitems={["Python", "Typescript"]}>
231
246
<Tabs.Tabtitle="Python">
232
247
248
+
The test suite manages LocalStack via [Docker Compose](https://docs.docker.com/compose/). Startup and teardown are fully automatic.
249
+
250
+
The suite seeds 3 buckets with 3 synthetic objects each, triggers `fetch_s3_buckets` manually, then polls S3 until every object has been deleted by the pipeline — validating the full fan-out chain end-to-end.
251
+
233
252
Make sure `boto3` is installed, then run from `sdks/python/`:
0 commit comments