Framework Adapters
The framework adapters specification defines how OJS SDKs integrate with web frameworks (Rails, Django, Express, Spring, etc.) to provide transactional enqueue, request-scoped context, and lifecycle hooks.
The Transactional Enqueue Problem
Section titled “The Transactional Enqueue Problem”A common pattern is to enqueue a job as part of a database transaction:
# WRONG: Job may be enqueued but transaction rolled backwith db.transaction(): order = Order.create(...) ojs.enqueue("order.fulfill", [order.id]) # Sent immediately raise SomeError() # Transaction rolls back, but job is already queued!The Outbox Pattern
Section titled “The Outbox Pattern”Framework adapters solve this using the outbox pattern:
- During the transaction, jobs are written to an
ojs_outboxtable instead of sent to the backend. - After the transaction commits, a background process reads the outbox and sends jobs to the backend.
- If the transaction rolls back, the outbox entries are rolled back too.
# CORRECT: Job is only sent if transaction commitswith db.transaction(): order = Order.create(...) ojs.enqueue("order.fulfill", [order.id]) # Written to outbox table# After commit: outbox processor sends to OJS backendAdapter Interface
Section titled “Adapter Interface”type FrameworkAdapter interface { // Enqueue within the current transaction context Enqueue(ctx context.Context, job *Job) error // Flush pending jobs (called after transaction commit) Flush(ctx context.Context) error // Discard pending jobs (called after transaction rollback) Discard(ctx context.Context) error}Framework Integration
Section titled “Framework Integration”Request Context
Section titled “Request Context”Adapters integrate with the web framework’s request lifecycle:
# Rails - jobs are flushed after each requestclass ApplicationController < ActionController::Base around_action :ojs_request_scope
private def ojs_request_scope OJS.with_request_scope { yield } endendDependency Injection
Section titled “Dependency Injection”Adapters register OJS clients in the framework’s DI container:
// Spring Boot@Beanpublic OJSClient ojsClient() { return OJS.client("http://localhost:8080");}Lifecycle Hooks
Section titled “Lifecycle Hooks”| Hook | Description |
|---|---|
on_request_start | Initialize request-scoped job buffer |
on_request_end | Flush buffered jobs |
on_transaction_commit | Send outbox entries to backend |
on_transaction_rollback | Discard outbox entries |
on_app_shutdown | Flush remaining outbox entries, shut down workers |
Language-Specific Examples
Section titled “Language-Specific Examples”OJS.configure do |config| config.backend_url = ENV["OJS_URL"] config.adapter = :active_record # Uses ActiveRecord outboxend
# In a controllerdef create ActiveRecord::Base.transaction do @order = Order.create!(order_params) OJS.enqueue("order.fulfill", [@order.id]) endendDjango
Section titled “Django”# In a viewfrom ojs import enqueuefrom django.db import transaction
@transaction.atomicdef create_order(request): order = Order.objects.create(...) enqueue("order.fulfill", [order.id]) # Uses Django outbox return JsonResponse({"id": order.id})Express
Section titled “Express”app.post('/orders', async (req, res) => { await db.transaction(async (tx) => { const order = await tx.insert('orders', req.body); await ojs.enqueue('order.fulfill', [order.id], { tx }); }); res.json({ success: true });});Interaction with Encryption
Section titled “Interaction with Encryption”When using the encryption extension with framework adapters, encryption MUST be applied before the job is inserted into the outbox table. This ensures the outbox does not contain plaintext sensitive data.