Skip to content

Commit 41676d7

Browse files
authored
feat: Bring up separate attachment delivery worker to handle file attachments instead of blocking the middleware (#19)
1 parent b09a599 commit 41676d7

File tree

25 files changed

+722
-222
lines changed

25 files changed

+722
-222
lines changed

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodejs 20

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ docker compose -f docker-compose.dev.yaml up ktu-bot-app --build
2929
docker compose -f docker-compose.dev.yaml up announcements-notify-worker --build
3030
docker compose -f docker-compose.dev.yaml up data-sync-worker --build
3131
docker compose -f docker-compose.dev.yaml up broadcasts-worker --build
32+
docker compose -f docker-compose.dev.yaml up attachment-delivery-worker --build
3233

3334
# Local development without Docker
3435
pnpm dev # Start bot with tsx hot reload
@@ -62,6 +63,7 @@ pnpm format:check # Check formatting
6263
pnpm workers:announcements-notify-worker-dev # Dev mode with hot reload
6364
pnpm workers:broadcasts-worker-dev
6465
pnpm workers:data-sync-worker-dev
66+
pnpm workers:attachment-delivery-worker-dev
6567
pnpm services:bull-board-dev # Bull Board dashboard
6668
```
6769

@@ -173,12 +175,14 @@ export class MyWorker extends BaseWorker<JobData> {
173175
- `DATA_SYNC_QUEUE` - Syncs KTU data (concurrency: 3, rate limited)
174176
- `ANNOUNCEMENTS_NOTIFY_QUEUE` - New announcement notifications (concurrency: 1)
175177
- `BROADCASTS_QUEUE` - Message delivery (concurrency: 1)
178+
- `ATTACHMENT_DELIVERY_QUEUE` - Non-blocking file downloads (concurrency: 2)
176179

177180
**Worker Implementations:**
178181

179182
- [src/workers/announcements/notify/](src/workers/announcements/notify/) - Monitors for new announcements, filters by course, creates broadcast jobs
180183
- [src/workers/broadcasts/](src/workers/broadcasts/) - Generic message delivery with rate limiting and error handling
181184
- [src/workers/data-sync/](src/workers/data-sync/) - Syncs KTU data to local DB for full-text search
185+
- [src/workers/attachment-delivery/](src/workers/attachment-delivery/) - Downloads and sends files asynchronously, prevents bot blocking
182186

183187
**Shared Utilities:**
184188

@@ -273,7 +277,7 @@ Middleware sequence is critical in [src/bot/bot.ts](src/bot/bot.ts:37):
273277
Each service exposes health endpoints:
274278

275279
- Bot: port 3000
276-
- Workers: ports 3001, 3002, 3003
280+
- Workers: ports 3001, 3002, 3003, 3004
277281
- Bull Board: port 3010
278282
- Check database and Redis connectivity
279283

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ The bot follows a microservices architecture where each component handles a spec
5656
| **Announcements Notify Worker** | Background worker | Monitors for new announcements using BullMQ scheduled jobs and sends filtered alerts to users |
5757
| **Broadcasts Worker** | Background worker | Handles queued broadcast message delivery |
5858
| **Data Sync Worker** | Background worker | Periodically syncs KTU data to local DB via BullMQ scheduled jobs to power full-text search |
59+
| **Attachment Delivery Worker** | Background worker | Downloads and sends files asynchronously to prevent bot blocking |
5960
| **Bull Board Service** | Monitoring service | Web dashboard for real-time queue monitoring and job management |
6061
| **PostgreSQL** | Database | Stores all data with Drizzle ORM for type-safe queries |
6162
| **Redis** | Queue | Powers BullMQ jobs |
@@ -147,6 +148,9 @@ docker compose -f docker/compose.dev.yaml up data-sync-worker --build
147148

148149
# Broadcasts worker
149150
docker compose -f docker/compose.dev.yaml up broadcasts-worker --build
151+
152+
# Attachment delivery worker
153+
docker compose -f docker/compose.dev.yaml up attachment-delivery-worker --build
150154
```
151155

152156
> [!TIP]

docker/compose.dev.yaml

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,54 @@ services:
216216
retries: 3
217217
start_period: 30s
218218

219+
attachment-delivery-worker:
220+
build:
221+
context: ..
222+
dockerfile: docker/Dockerfile.dev
223+
container_name: attachment-delivery-worker
224+
env_file:
225+
- ../env/dev/bot.env
226+
- ../env/dev/db.env
227+
- ../env/dev/redis.env
228+
- ../env/dev/api.env
229+
- ../env/dev/attachment-delivery-worker.env
230+
- ../env/dev/logging.env
231+
- ../env/dev/.env
232+
ports:
233+
- "3004:3004"
234+
volumes:
235+
- ..:/app
236+
- /app/node_modules
237+
depends_on:
238+
ktu-bot-postgres:
239+
condition: service_healthy
240+
restart: true
241+
ktu-bot-redis:
242+
condition: service_healthy
243+
restart: true
244+
ktu-bot-db-migrations:
245+
condition: service_completed_successfully
246+
networks:
247+
- ktu-bot-network
248+
command: ["pnpm", "workers:attachment-delivery-worker-dev"]
249+
restart: unless-stopped
250+
healthcheck:
251+
test:
252+
[
253+
"CMD",
254+
"curl",
255+
"-f",
256+
"-s",
257+
"-S",
258+
"--max-time",
259+
"5",
260+
"http://localhost:3004/health",
261+
]
262+
interval: 60s
263+
timeout: 10s
264+
retries: 3
265+
start_period: 30s
266+
219267
bull-board-service:
220268
build:
221269
context: ..
@@ -232,7 +280,7 @@ services:
232280
- ../env/dev/broadcasts-worker.env
233281
- ../env/dev/announcements-notify-worker.env
234282
- ../env/dev/data-sync-worker.env
235-
- ../env/dev/logging.env
283+
- ../env/dev/attachment-delivery-worker.env
236284
- ../env/dev/.env
237285
ports:
238286
- "3010:3010"

docker/compose.staging.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,51 @@ services:
192192
max-size: "10m"
193193
max-file: "3"
194194

195+
attachment-delivery-worker:
196+
image: ktu-bot:latest
197+
container_name: attachment-delivery-worker
198+
env_file:
199+
- ../env/staging/.env
200+
- ../env/staging/attachment-delivery-worker.env
201+
networks:
202+
- ktu-bot-network
203+
depends_on:
204+
ktu-bot-db-migrations:
205+
condition: service_completed_successfully
206+
ports:
207+
- "3004:3004"
208+
command: ["pnpm", "workers:attachment-delivery-worker"]
209+
restart: unless-stopped
210+
deploy:
211+
resources:
212+
limits:
213+
memory: 512M
214+
cpus: "0.5"
215+
reservations:
216+
memory: 256M
217+
cpus: "0.25"
218+
healthcheck:
219+
test:
220+
[
221+
"CMD",
222+
"curl",
223+
"-f",
224+
"-s",
225+
"-S",
226+
"--max-time",
227+
"5",
228+
"http://localhost:3004/health",
229+
]
230+
interval: 60s
231+
timeout: 10s
232+
retries: 3
233+
start_period: 30s
234+
logging:
235+
driver: "json-file"
236+
options:
237+
max-size: "10m"
238+
max-file: "3"
239+
195240
bull-board-service:
196241
image: ktu-bot:latest
197242
container_name: bull-board-service
@@ -206,6 +251,8 @@ services:
206251
condition: service_healthy
207252
data-sync-worker:
208253
condition: service_healthy
254+
attachment-delivery-worker:
255+
condition: service_healthy
209256
ports:
210257
- "3010:3010"
211258
command: ["pnpm", "services:bull-board"]
@@ -239,3 +286,7 @@ services:
239286
options:
240287
max-size: "10m"
241288
max-file: "3"
289+
290+
networks:
291+
ktu-bot-network:
292+
driver: bridge

docker/compose.yaml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,51 @@ services:
192192
max-size: "10m"
193193
max-file: "3"
194194

195+
attachment-delivery-worker:
196+
image: ktu-bot:latest
197+
container_name: attachment-delivery-worker
198+
env_file:
199+
- ../env/prod/.env
200+
- ../env/prod/attachment-delivery-worker.env
201+
networks:
202+
- ktu-bot-network
203+
depends_on:
204+
ktu-bot-db-migrations:
205+
condition: service_completed_successfully
206+
ports:
207+
- "3004:3004"
208+
command: ["pnpm", "workers:attachment-delivery-worker"]
209+
restart: unless-stopped
210+
deploy:
211+
resources:
212+
limits:
213+
memory: 512M
214+
cpus: "0.5"
215+
reservations:
216+
memory: 256M
217+
cpus: "0.25"
218+
healthcheck:
219+
test:
220+
[
221+
"CMD",
222+
"curl",
223+
"-f",
224+
"-s",
225+
"-S",
226+
"--max-time",
227+
"5",
228+
"http://localhost:3004/health",
229+
]
230+
interval: 60s
231+
timeout: 10s
232+
retries: 3
233+
start_period: 30s
234+
logging:
235+
driver: "json-file"
236+
options:
237+
max-size: "10m"
238+
max-file: "3"
239+
195240
bull-board-service:
196241
image: ktu-bot:latest
197242
container_name: bull-board-service
@@ -206,6 +251,8 @@ services:
206251
condition: service_healthy
207252
data-sync-worker:
208253
condition: service_healthy
254+
attachment-delivery-worker:
255+
condition: service_healthy
209256
ports:
210257
- "3010:3010"
211258
command: ["pnpm", "services:bull-board"]

docs/working.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ graph TB
3434
TG -->|Webhook/Long Polling| Bot
3535
Bot -->|Fetch Data| KTU
3636
Bot -->|Store/Retrieve| DB
37-
Bot -->|Cache/Queue| Redis
37+
Bot -->|Cache| Redis
38+
Bot -->|Queue Attachments| Queue
3839
Bot -->|Response| TG
3940
TG -->|Messages| User
4041
@@ -65,6 +66,7 @@ graph TB
6566
NotifyWorker[📢 Announcements<br/>Notify Worker<br/>Port: 3001]
6667
BroadcastWorker[📨 Broadcasts<br/>Worker<br/>Port: 3002]
6768
SyncWorker[🔄 Data Sync<br/>Worker<br/>Port: 3003]
69+
AttachmentWorker[📦 Attachment<br/>Delivery Worker<br/>Port: 3004]
6870
end
6971
7072
subgraph "Message Queue"
@@ -92,12 +94,16 @@ graph TB
9294
NotifyWorker -->|Add Jobs| Queue
9395
9496
Queue -->|Process Jobs| BroadcastWorker
97+
Queue -->|Process Attachments| AttachmentWorker
9598
BroadcastWorker -->|Send Messages| TG
9699
BroadcastWorker -->|Update Status| DB
97100
98101
SyncWorker -->|Fetch All Data| KTU
99102
SyncWorker -->|Sync| DB
100103
104+
Bot -->|Queue Attachments| Queue
105+
AttachmentWorker -->|Download & Send| TG
106+
101107
Queue -.->|Uses| Redis
102108
103109
style User fill:#4a90e2,stroke:#2e5c8a,color:#fff
@@ -198,9 +204,32 @@ When users perform inline searches, their queries hit this local copy instead of
198204

199205
The BullMQ-based approach makes this much more resilient than traditional cron scheduling. Check out [`src/workers/data-sync/`](../src/workers/data-sync/) for the implementation.
200206

207+
### Attachment Delivery Worker 📦
208+
209+
This worker handles file downloads and deliveries asynchronously, preventing the bot from being blocked by large file downloads. When users request attachments (calendars, timetables, announcements), the bot immediately queues the request and returns to serving other users. Here's how it works:
210+
211+
1. **Job Creation**: When a user requests files, the bot creates a job containing all attachment metadata and immediately returns
212+
- Bot sends a friendly status message like _"⏳ Preparing your calendar... I'll send it shortly!"_
213+
- The bot doesn't wait for downloads to complete
214+
2. **Background Processing**: The worker picks up jobs from the queue and downloads all attachments
215+
- Uses **all-or-nothing delivery** - if any file fails to download, nothing is sent to ensure users get complete data
216+
- Downloads happen asynchronously without blocking other users
217+
3. **Media Group Delivery**: Once all files are ready, sends them as media groups (batches of up to 10 files per Telegram API call)
218+
- Much faster than individual file delivery
219+
- Single caption listing all attachment names for clarity
220+
4. **Graceful Error Handling**: Handles various failure scenarios without crashing
221+
- If user blocks the bot, marks their chat as kicked and removes subscriptions
222+
- If user deactivates account, cleans up their data from the database
223+
- If rate limited by Telegram, pauses the entire queue for the specified duration, then resumes processing
224+
5. **Status Updates**: Deletes the loading message once delivery is complete (or on failure)
225+
- No chat pollution - the status message disappears after completion
226+
- User receives their files cleanly without extra messages
227+
228+
This worker has a `concurrency` of `2` to prevent overwhelming Telegram's rate limits while still processing requests efficiently. All implementation lives in [`src/workers/attachment-delivery/`](../src/workers/attachment-delivery/)
229+
201230
## Health Checks and Monitoring
202231

203-
Each service exposes a health check endpoint (bot on port `3000`, workers on `3001-3003`) that verifies the service is running and can connect to its dependencies like the database and Redis. This enables zero-downtime deployments and automatic restarts if something goes wrong. The health check utility is in [`src/utils/healthCheck.ts`](../src/utils//healthCheck.ts) if you want to see how it works.
232+
Each service exposes a health check endpoint (bot on port `3000`, workers on `3001-3004`) that verifies the service is running and can connect to its dependencies like the database and Redis. This enables zero-downtime deployments and automatic restarts if something goes wrong. The health check utility is in [`src/utils/healthCheck.ts`](../src/utils//healthCheck.ts) if you want to see how it works.
204233

205234
### Queue Monitoring with Bull Board 📊
206235

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Attachment Delivery Worker configs
2+
ATTACHMENT_DELIVERY_WORKER_HEALTHCHECK_PORT=3004
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Attachment Delivery Worker configs
2+
ATTACHMENT_DELIVERY_WORKER_HEALTHCHECK_PORT=3004
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Attachment Delivery Worker configs
2+
ATTACHMENT_DELIVERY_WORKER_HEALTHCHECK_PORT=3004

0 commit comments

Comments
 (0)