Skip to content

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

bash
pnpm --filter @playpals/ogun dev
pnpm --filter @playpals/ogun test

It 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:

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

Rendering diagram…

Image variants

Variants depend on the parent post's type:

Post typeVariantWidthHeightQuality
story_imagestory1080192085
story_imagethumb32056878
photo, carousel, photos, defaultthumb32040078
medium72090082
full1080135085

Each output is:

ts
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

ts
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:

ts
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

VariablePurpose
DATABASE_URLPrisma
REDIS_URL or REDIS_HOST + REDIS_PORTQueue connection; REDIS_URL wins
SPACES_REGION, SPACES_ENDPOINT, SPACES_URL, SPACES_ACCESS_KEY, SPACES_SECRET_KEY, SPACES_BUCKETObject storage
MUX_ACCESS_TOKEN, MUX_SECRET_KEYVideo
REALTIME_SERVICE_URL, REALTIME_ADMIN_TOKENRealtime notification of completion

The Redis connection sets maxRetriesPerRequest: null, which BullMQ requires.

Deployment

PropertyValue
Manifestinfra/k8s/ogun-deployment.yaml
Replicas2
ProbesNone
IngressNone
bash
./scripts/build-ogun.sh [tag]

Known issues

The realtime completion event never arrives in production

ogun's helper does not add the /admin prefix:

ts
fetch(`${process.env.REALTIME_SERVICE_URL}${path}`, …)   // ogun
fetch(`${process.env.REALTIME_SERVICE_URL}/admin${path}`, …)   // api

Both 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

bash
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 -1

Logs are structured per module (worker, processMedia, processImage, processVideo) and carry mediaId, so a single item is traceable end to end.

Internal documentation — PlayPalz platform