ogun — Media Worker
@playpals/ogun · no HTTP port · apps/ogun
Ogun symbolizes raw power, machinery, and transformation.
A pure BullMQ worker. It consumes the media-processing queue, generates image variants with Sharp, creates Mux assets for video, and writes results back to the database. It serves no HTTP traffic. Two replicas in production.
Running it
pnpm --filter @playpals/ogun dev
pnpm --filter @playpals/ogun testIt needs DATABASE_URL, Redis, and valid SPACES_* credentials. Without object storage access it will pick up every job and fail it.
The worker
apps/ogun/src/index.ts:
const mediaWorker = new Worker(
PROCESS_MEDIA_QUEUE,
async (job) => { await processMedia(job.data.mediaId); },
{ connection: redis, concurrency: 5 },
);Five jobs in flight per process. The queue name and retry policy come from @playpals/queue, so the API and the worker cannot drift apart.
Job flow
Image variants
Variants depend on the parent post's type:
| Post type | Variant | Width | Height | Quality |
|---|---|---|---|---|
story_image | story | 1080 | 1920 | 85 |
story_image | thumb | 320 | 568 | 78 |
photo, carousel, photos, default | thumb | 320 | 400 | 78 |
medium | 720 | 900 | 82 | |
full | 1080 | 1350 | 85 |
Each output is:
sharp(inputBuffer)
.rotate() // honour EXIF orientation
.resize(width, height, {
fit: "cover",
withoutEnlargement: true, // never upscale a small original
position: sharp.strategy.attention, // crop toward the subject
})
.jpeg({ quality, mozjpeg: true })Uploaded public-read with Cache-Control: public, max-age=31536000, immutable, keyed variants/<mediaId>/<type>.jpg. Immutable is safe because the key contains the media id.
Variants are upserted — a re-run updates existing rows rather than duplicating them, so replaying a job is harmless.
Video
const asset = await mux.video.assets.create({
inputs: [{ url: media.originalUrl }],
playback_policies: ["public"],
video_quality: "basic",
});The job finishes as soon as Mux accepts the asset. Media.status becomes ready only when Mux calls the API's webhook. See Media Pipeline.
Failure handling
Image failures write the error and re-throw so BullMQ retries:
await prisma.media.update({
where: { id: media.id },
data: { status: "failed", errorMessage: error.message },
});
throw error;Three attempts, exponential backoff from 2 s, then the job lands in the failed set. Media.status and errorMessage tell you why without touching Redis.
Configuration
| Variable | Purpose |
|---|---|
DATABASE_URL | Prisma |
REDIS_URL or REDIS_HOST + REDIS_PORT | Queue connection; REDIS_URL wins |
SPACES_REGION, SPACES_ENDPOINT, SPACES_URL, SPACES_ACCESS_KEY, SPACES_SECRET_KEY, SPACES_BUCKET | Object storage |
MUX_ACCESS_TOKEN, MUX_SECRET_KEY | Video |
REALTIME_SERVICE_URL, REALTIME_ADMIN_TOKEN | Realtime notification of completion |
The Redis connection sets maxRetriesPerRequest: null, which BullMQ requires.
Deployment
| Property | Value |
|---|---|
| Manifest | infra/k8s/ogun-deployment.yaml |
| Replicas | 2 |
| Probes | None |
| Ingress | None |
./scripts/build-ogun.sh [tag]Known issues
The realtime completion event never arrives in production
ogun's helper does not add the /admin prefix:
fetch(`${process.env.REALTIME_SERVICE_URL}${path}`, …) // ogun
fetch(`${process.env.REALTIME_SERVICE_URL}/admin${path}`, …) // apiBoth deployments set REALTIME_SERVICE_URL=http://playpalz-esu:4010, so ogun POSTs to /emit/media/processing/update — a route esu does not have. The .catch(() => {}) on the call hides the 404. Image-ready updates reach the app only through the polling fallback.
No health probe
Kubernetes cannot tell whether the worker is consuming jobs. A worker wedged on a Redis connection looks healthy. A minimal HTTP server reporting queue connectivity would make it restartable.
- Scaling is by replica count; the work is CPU-bound in Sharp. Watch Mux rate limits before scaling video volume.
- Originals are never deleted after processing. Storage grows without bound.
Operating
kubectl logs -l app=playpalz-ogun --tail=100 -f
# queue state
redis-cli -a <password> KEYS 'bull:media-processing:*'
redis-cli -a <password> LLEN 'bull:media-processing:wait'
redis-cli -a <password> LRANGE 'bull:media-processing:failed' 0 -1Logs are structured per module (worker, processMedia, processImage, processVideo) and carry mediaId, so a single item is traceable end to end.
