Skip to content

Commit 349dcd7

Browse files
committed
Import Render skill resources
1 parent e48c407 commit 349dcd7

33 files changed

Lines changed: 3388 additions & 1 deletion

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Graceful shutdown for Render workers
2+
3+
Render stops worker instances during **deploys**, **manual restarts**, and **scale-in** events. Your process must **exit cleanly** within the configured window or work may be **lost** or **duplicated** (depending on your queue’s ack/retry semantics).
4+
5+
## Platform behavior
6+
7+
1. Render sends **`SIGTERM`** to your process.
8+
2. The platform waits up to **`maxShutdownDelaySeconds`** (**1–300**, **default 30**).
9+
3. If the process is still running, Render sends **`SIGKILL`** (not catchable).
10+
11+
Configure **`maxShutdownDelaySeconds`** in the **Dashboard** (service settings) or in **`render.yaml`** on the worker service. Set it to cover your **longest job** you are willing to let complete during shutdown (plus buffer for flushing metrics, closing DB pools, etc.).
12+
13+
## General pattern
14+
15+
1. **Stop accepting new jobs** — stop the consumer loop, pause polling, or drain the framework’s internal fetch.
16+
2. **Finish the current job** or **checkpoint** durable progress so another worker can resume safely.
17+
3. **Close connections** — Redis/Postgres pools, HTTP clients.
18+
4. **Exit with code 0** when done.
19+
20+
## Python
21+
22+
**Low-level handler**
23+
24+
```python
25+
import signal
26+
import sys
27+
28+
def handle_sigterm(signum, frame):
29+
# set a flag; main loop checks it and stops dequeuing
30+
global shutting_down
31+
shutting_down = True
32+
33+
signal.signal(signal.SIGTERM, handle_sigterm)
34+
```
35+
36+
**Celery** — use lifecycle signals such as **`worker_shutting_down`** to run cleanup; ensure tasks honor a **soft time limit** or cooperative cancel flag so shutdown can finish within **`maxShutdownDelaySeconds`**.
37+
38+
## Ruby (Sidekiq)
39+
40+
Sidekiq **handles SIGTERM** by default: it stops fetching new work and waits for in-flight jobs up to a **configurable timeout** (`:timeout` in Sidekiq options, in seconds). Align that timeout with Render’s **`maxShutdownDelaySeconds`** (Sidekiq timeout should be **** platform delay minus a small margin).
41+
42+
## Node.js
43+
44+
```javascript
45+
let accept = true;
46+
47+
process.on("SIGTERM", async () => {
48+
accept = false;
49+
await worker.close(); // BullMQ: stops accepting, waits for active jobs
50+
process.exit(0);
51+
});
52+
```
53+
54+
**BullMQ** — prefer **`worker.close()`** (and **`queue.close()`** where applicable) so active jobs complete per library defaults; tune **`stalledInterval`** / job locks if you need stricter bounds.
55+
56+
## Go
57+
58+
Use **`signal.NotifyContext`** (or `signal.Notify` + `context.WithCancel`) to cancel a root context passed into your consumer loop and job handlers; wait on **`sync.WaitGroup`** or channels until in-flight work finishes, then exit.
59+
60+
```go
61+
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
62+
defer stop()
63+
// run consumer until ctx.Done(), then drain workers
64+
```
65+
66+
## Anti-patterns
67+
68+
- **Ignoring SIGTERM** — the process survives until **`SIGKILL`**, often **mid-job**, causing **lost work** or **stuck** queue entries.
69+
- **`maxShutdownDelaySeconds` too low** for your p95 job duration — frequent **hard kills** and retries.
70+
- **No idempotency** — if a job is retried after an ambiguous failure at shutdown, **duplicate side effects** can occur.
71+
72+
## Checklist
73+
74+
| Item | Action |
75+
|------|--------|
76+
| Delay | Set **`maxShutdownDelaySeconds`** ≥ longest graceful completion you need |
77+
| Consumer | On SIGTERM, **stop dequeuing** first |
78+
| Jobs | **Idempotent** handlers or explicit **checkpoints** |
79+
| Framework | Use built-in **drain** / **close** APIs (Sidekiq, BullMQ, Celery signals) where available |
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
# Queue framework setup on Render
2+
3+
Minimal patterns for running each stack as a **`type: worker`** service. Replace regions, plans, and commands to match your repo.
4+
5+
**Shared Key Value (Redis-style) wiring** — add once, reference from every consumer that needs the broker:
6+
7+
```yaml
8+
services:
9+
- type: keyvalue
10+
name: jobs
11+
region: oregon
12+
plan: starter
13+
ipAllowList: []
14+
```
15+
16+
**Env var** (worker or web that enqueues):
17+
18+
```yaml
19+
envVars:
20+
- key: REDIS_URL
21+
fromService:
22+
name: jobs
23+
type: keyvalue
24+
property: connectionString
25+
```
26+
27+
Set Key Value **maxmemory policy** to **`noeviction`** in the Dashboard so queue keys are never evicted like cache entries.
28+
29+
---
30+
31+
## Celery (Python)
32+
33+
**Install**
34+
35+
```bash
36+
pip install "celery[redis]"
37+
```
38+
39+
**Minimal app** (`tasks.py` / package root):
40+
41+
```python
42+
import os
43+
from celery import Celery
44+
45+
app = Celery("tasks", broker=os.environ["REDIS_URL"])
46+
47+
@app.task
48+
def add(x, y):
49+
return x + y
50+
```
51+
52+
**Start command** (adjust module path):
53+
54+
```bash
55+
celery -A tasks worker --loglevel=info
56+
```
57+
58+
**Blueprint (worker)**
59+
60+
```yaml
61+
- type: worker
62+
name: celery-worker
63+
runtime: python
64+
region: oregon
65+
plan: starter
66+
buildCommand: pip install -r requirements.txt
67+
startCommand: celery -A tasks worker --loglevel=info
68+
envVars:
69+
- key: REDIS_URL
70+
fromService:
71+
name: jobs
72+
type: keyvalue
73+
property: connectionString
74+
```
75+
76+
---
77+
78+
## Sidekiq (Ruby)
79+
80+
**Gemfile**
81+
82+
```ruby
83+
gem "sidekiq"
84+
```
85+
86+
**Initializer** (`config/initializers/sidekiq.rb` or equivalent):
87+
88+
```ruby
89+
Sidekiq.configure_server do |config|
90+
config.redis = { url: ENV.fetch("REDIS_URL") }
91+
end
92+
93+
Sidekiq.configure_client do |config|
94+
config.redis = { url: ENV.fetch("REDIS_URL") }
95+
end
96+
```
97+
98+
**Start command**
99+
100+
```bash
101+
bundle exec sidekiq
102+
```
103+
104+
**Blueprint (worker)**
105+
106+
```yaml
107+
- type: worker
108+
name: sidekiq
109+
runtime: ruby
110+
region: oregon
111+
plan: starter
112+
buildCommand: bundle install
113+
startCommand: bundle exec sidekiq
114+
envVars:
115+
- key: REDIS_URL
116+
fromService:
117+
name: jobs
118+
type: keyvalue
119+
property: connectionString
120+
```
121+
122+
---
123+
124+
## BullMQ (Node.js)
125+
126+
**Install**
127+
128+
```bash
129+
npm install bullmq ioredis
130+
```
131+
132+
**Minimal worker** (`worker.js`):
133+
134+
```javascript
135+
const { Worker } = require("bullmq");
136+
const IORedis = require("ioredis");
137+
138+
const connection = new IORedis(process.env.REDIS_URL, {
139+
maxRetriesPerRequest: null,
140+
});
141+
142+
const worker = new Worker("myqueue", async (job) => job.data, { connection });
143+
```
144+
145+
**Start command**
146+
147+
```bash
148+
node worker.js
149+
```
150+
151+
**Blueprint (worker)**
152+
153+
```yaml
154+
- type: worker
155+
name: bullmq-worker
156+
runtime: node
157+
region: oregon
158+
plan: starter
159+
buildCommand: npm ci
160+
startCommand: node worker.js
161+
envVars:
162+
- key: REDIS_URL
163+
fromService:
164+
name: jobs
165+
type: keyvalue
166+
property: connectionString
167+
```
168+
169+
---
170+
171+
## Asynq (Go)
172+
173+
**Module**
174+
175+
```bash
176+
go get github.com/hibiken/asynq
177+
```
178+
179+
**Minimal worker** (simplified):
180+
181+
```go
182+
package main
183+
184+
import (
185+
"log"
186+
"os"
187+
188+
"github.com/hibiken/asynq"
189+
)
190+
191+
func main() {
192+
redisOpt, err := asynq.ParseRedisURI(os.Getenv("REDIS_URL"))
193+
if err != nil {
194+
log.Fatal(err)
195+
}
196+
srv := asynq.NewServer(redisOpt, asynq.Config{Concurrency: 10})
197+
mux := asynq.NewServeMux()
198+
// mux.HandleFunc("task:type", handler)
199+
if err := srv.Run(mux); err != nil {
200+
log.Fatal(err)
201+
}
202+
}
203+
```
204+
205+
**Start command** (after `go build` in `buildCommand` or run `go run`):
206+
207+
```bash
208+
./worker
209+
```
210+
211+
**Blueprint (worker)**
212+
213+
```yaml
214+
- type: worker
215+
name: asynq-worker
216+
runtime: go
217+
region: oregon
218+
plan: starter
219+
buildCommand: go build -o worker ./cmd/worker
220+
startCommand: ./worker
221+
envVars:
222+
- key: REDIS_URL
223+
fromService:
224+
name: jobs
225+
type: keyvalue
226+
property: connectionString
227+
```
228+
229+
---
230+
231+
## Oban (Elixir)
232+
233+
Oban uses **PostgreSQL** as its queue store—**not** Redis. Provision a Render **PostgreSQL** database and wire **`DATABASE_URL`**.
234+
235+
**Config** (`config/runtime.exs` or `config/prod.exs` pattern):
236+
237+
```elixir
238+
config :my_app, Oban,
239+
repo: MyApp.Repo,
240+
queues: [default: 10]
241+
```
242+
243+
**Start command** (typical Release):
244+
245+
```bash
246+
_build/prod/rel/my_app/bin/my_app start
247+
```
248+
249+
**Blueprint (worker)** — `fromDatabase` for `DATABASE_URL`:
250+
251+
```yaml
252+
databases:
253+
- name: app-db
254+
plan: basic-256mb
255+
region: oregon
256+
257+
services:
258+
- type: worker
259+
name: oban-worker
260+
runtime: elixir
261+
region: oregon
262+
plan: starter
263+
buildCommand: mix deps.get && mix release
264+
startCommand: _build/prod/rel/my_app/bin/my_app start
265+
envVars:
266+
- key: DATABASE_URL
267+
fromDatabase:
268+
name: app-db
269+
property: connectionString
270+
```
271+
272+
Do **not** point Oban at Key Value unless you are using an **experimental or custom** backend; default Oban is **Postgres-only**.

0 commit comments

Comments
 (0)