How the OpenMAIC Render-Service Coordinates Video Rendering Jobs with the RenderCoordinator
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) 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) owns the job queue, concurrency limits, and lifecycle events, while the RenderExecutor (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 (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:
- Generates a UUID to serve as the job identifier
- Creates a persistent record in the
JobStorewith initial statuspending - Attaches an
AbortControllerto support cancellation - 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 bymakeProjectDir()outputPath: The destination for the final video fileoptions: Rendering parameters extracted from the client requestsignal: TheAbortSignalfor cancellation supportdeadlineMs: A timeout thresholdonProgress: 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_submittedwhen the job enters the queuerender_job_startedwhen execution beginsrender_job_finishedupon 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
runningcounter - 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. This hook manages the HTTP communication with the render-service endpoint and provides reactive access to the job status stored in the JobStore.
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 theJobStoreand pushes jobs onto a FIFO queue managed bypump(). - 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 (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, 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →