# How the OpenMAIC Render-Service Coordinates Video Rendering Jobs with the RenderCoordinator

> Discover how the OpenMAIC render-service uses the RenderCoordinator to manage video rendering jobs. Learn about admission control, queueing, and execution management.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: internals
- Published: 2026-09-13

---

**The OpenMAIC render-service delegates the entire video rendering lifecycle to the RenderCoordinator, which acts as a central orchestrator handling admission control, FIFO queueing, concurrency throttling via a semaphore-backed execution gate, and deterministic cleanup, while the RenderExecutor performs the actual Chromium-based rendering work.**

The OpenMAIC platform implements a clear separation of concerns between the HTTP layer and the rendering engine. When a client initiates a video export, the `render-service` acts as a thin façade that prepares the project environment and hands off execution to the **RenderCoordinator**. According to the OpenMAIC source code, this coordinator maintains all stateful scheduling logic, ensuring that resource-intensive Chromium processes are managed safely and fairly across multiple concurrent users.

## Architecture Overview

The coordination model follows a producer-consumer pattern with strict admission controls. The `render-service` ([`lib/server/render-service.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/server/render-service.ts)) exposes API endpoints that extract user archives and prepare `RenderOptions`, but immediately delegates to the coordinator for any stateful operations. The **RenderCoordinator** ([`render-service/src/render-coordinator.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/render-service/src/render-coordinator.ts)) owns the job queue, concurrency limits, and lifecycle events, while the **RenderExecutor** ([`render-service/src/render-executor.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/render-service/src/render-executor.ts)) handles the actual headless Chromium automation.

## Admission Control and Rate Limiting

Before accepting a job, the coordinator validates resource availability through the `reserve()` method. When a client sends a request to `POST /api/render/video`, the handler invokes `coordinator.reserve(identity)`, where *identity* is typically the client IP or a user token.

As implemented in [`render-coordinator.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/render-coordinator.ts) (lines 51–64), this method enforces two caps:
- A global queue limit (`maxQueue`) that prevents system overload
- A per-identity limit (`maxJobsPerUser`) that ensures fair scheduling across users

If either threshold is exceeded, the coordinator throws a `RenderRejectedError`, which the API layer maps to an HTTP 429 (Too Many Requests) response. This guarantees that the system never accepts work it cannot schedule.

## Job Submission and Queueing

Upon successful reservation, the handler invokes `coordinator.submit(reservation, projectDir, options)` (lines 84–102). This method performs the following atomic operations:
1. Generates a UUID to serve as the job identifier
2. Creates a persistent record in the `JobStore` with initial status `pending`
3. Attaches an `AbortController` to support cancellation
4. Pushes the job onto an internal FIFO queue

The coordinator then immediately calls `pump()` (lines 59–65) to evaluate whether the job can transition from queued to running. This method ensures the number of concurrently active jobs never exceeds the configured `maxConcurrency` limit.

## Concurrency Throttling and Execution

The **RenderCoordinator** regulates access to Chromium instances using a `Semaphore` named `executionGate`. When `pump()` selects a job for execution, it invokes `runWithExecutionSlot()` (lines 9–12), which acquires a permit from the semaphore before proceeding.

Within the execution slot, the coordinator calls `executor.execute()` with a structured payload containing:
- `projectDir`: The temporary workspace created by `makeProjectDir()`
- `outputPath`: The destination for the final video file
- `options`: Rendering parameters extracted from the client request
- `signal`: The `AbortSignal` for cancellation support
- `deadlineMs`: A timeout threshold
- `onProgress`: A callback function for status updates

The `RenderExecutor` launches a headless Chromium process, drives the slide deck presentation, records the video output, and periodically invokes the progress callback to report rendering stages.

## Progress Tracking and Lifecycle Events

During execution, each progress update flows through the callback into the `JobStore`, updating fields such as `status: 'running'`, `progress` percentage, and `currentStage`. Simultaneously, the coordinator emits structured lifecycle events via the `RenderEventSink`, including:
- `render_job_submitted` when the job enters the queue
- `render_job_started` when execution begins
- `render_job_finished` upon successful completion

These events enable real-time status polling from the client-side `useRenderVideo` hook.

## Completion and Cleanup

When the `RenderExecutor` resolves successfully, the coordinator stores the output artifact via `ArtifactStore.put()`, updates the job record to `succeeded`, and emits the completion event. If the executor throws an error or the job is cancelled, `finishNonSuccess()` (lines 101–124) records the failure code and triggers cleanup.

Regardless of outcome, the coordinator guarantees cleanup through `cleanupProject()` (lines 14–22), which:
- Recursively deletes the temporary project directory
- Removes intermediate plan files
- Releases the `AbortController`
- Decrements the per-identity counter and the global `running` counter
- Allows `pump()` to schedule the next queued job

## Client-Side Integration

The front-end initiates this entire coordination flow through the `useRenderVideo` hook defined in [`lib/video-export-app/use-render-video.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/video-export-app/use-render-video.ts). This hook manages the HTTP communication with the render-service endpoint and provides reactive access to the job status stored in the `JobStore`.

```tsx
import { useRenderVideo } from '@/lib/video-export-app/use-render-video';

function ExportButton({ archiveBlob }: { archiveBlob: Blob }) {
  const { startRender, jobId, status, error } = useRenderVideo();

  const handleClick = async () => {
    const form = new FormData();
    form.append('archive', archiveBlob);
    await startRender(form); // Triggers the coordinator pipeline
  };

  return (
    <>
      <button onClick={handleClick}>Export Video</button>
      {jobId && <p>Job ID: {jobId}</p>}
      {status && <p>Status: {status}</p>}
      {error && <p>Error: {error}</p>}
    </>
  );
}

```

This React hook ultimately performs the `POST /api/render/video` request that triggers the admission checks and coordination logic described above.

## Summary

- The **render-service** acts as a stateless HTTP façade that prepares project directories and delegates all scheduling to the **RenderCoordinator**.
- **Admission control** via `reserve()` enforces global and per-user queue limits, rejecting excess load with HTTP 429 errors.
- **Job submission** via `submit()` creates persistent records in the `JobStore` and pushes jobs onto a FIFO queue managed by `pump()`.
- **Concurrency throttling** uses a `Semaphore` (`executionGate`) to bound active Chromium instances, guaranteeing system stability.
- The **RenderExecutor** performs the actual video rendering while the coordinator handles progress tracking, event emission, and artifact storage.
- **Deterministic cleanup** via `cleanupProject()` ensures temporary resources are freed regardless of success or failure.

## Frequently Asked Questions

### What happens when the RenderCoordinator's queue reaches capacity?

When the number of pending jobs reaches the `maxQueue` limit, or when a specific user exceeds their `maxJobsPerUser` allocation, the `reserve()` method throws a `RenderRejectedError`. According to the implementation in [`render-coordinator.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/render-coordinator.ts) (lines 51–64), this error propagates to the client as an HTTP 429 response, signaling that the system is temporarily overloaded and the request should be retried later.

### How does the RenderCoordinator prevent too many Chromium instances from running simultaneously?

The coordinator maintains a `Semaphore` instance called `executionGate` that acts as a bounded pool of execution slots. Before invoking the `RenderExecutor`, the coordinator calls `runWithExecutionSlot()`, which acquires a permit from the semaphore. As shown in lines 9–12 of [`render-coordinator.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/render-coordinator.ts), this mechanism ensures that the number of concurrent Chromium processes never exceeds the configured `maxConcurrency` threshold, protecting server resources from exhaustion.

### How does the system clean up temporary files after a video render completes?

The coordinator guarantees cleanup through the `cleanupProject()` method (lines 14–22), which executes regardless of whether the job succeeds, fails, or is cancelled. This method recursively deletes the temporary project directory created by `makeProjectDir()`, removes any intermediate plan files, and releases the `AbortController` associated with the job. The cleanup also decrements the per-identity and global `running` counters, allowing the `pump()` scheduler to admit new work.

### How can a client track the progress of a submitted video rendering job?

After submission, clients can poll job status through the `JobStore`, which the coordinator updates continuously via progress callbacks from the `RenderExecutor`. The coordinator also emits lifecycle events such as `render_job_started` and `render_job_finished` through the `RenderEventSink`. The `useRenderVideo` React hook abstracts this polling mechanism, providing reactive access to the job ID, current status, progress percentage, and any error messages returned by the coordination layer.