class RubyLLM::Browser::Rails::OperationJob
Active Job wrapper for one bounded browser operation.
The job is deliberately not a scheduler: it never reserves a second operation and never retries an effectful call. The host reserves (or attaches to) a durable operation with {.enqueue} and this job attaches to that exact row. On delivery it leases the row (bumping its fence), refuses the wrong actor or an already-terminal operation, and runs the read-only operation through the {Engine}. A duplicated or resumed delivery finds a non-‘:reserved` row and returns without a remote call; a worker that lost the lease is refused as stale.
The context (backend, source policy, stores, budget, consent) is host-supplied and cannot be serialized, so the job builds it from a configured factory:
RubyLLM::Browser::Rails.context_factory = lambda do |actor_id:, run_id:| RubyLLM::Browser::Context.new( actor_id: actor_id, run_id: run_id, backend: qualified_backend, source_policy: host_policy, operation_store: RubyLLM::Browser::Rails.operation_store ) end
Attributes
->(actor_id:, run_id:) { RubyLLM::Browser::Context }
Overrides the module store for this job class. Defaults to the module store.
Public Class Methods
Reserves (or attaches to) the durable operation for idempotency_key and enqueues exactly one job for it. A second call with the same key attaches to the same operation instead of reserving a new one.
@return [Operation] the reserved or attached operation
# File lib/ruby_llm/browser/rails/operation_job.rb, line 40 def enqueue(actor_id:, run_id:, kind:, url:, budget:, idempotency_key: nil, instruction: nil, schema: nil, max_bytes: nil, store: nil, now: nil) configured_store = operation_store || RubyLLM::Browser::Rails.operation_store store ||= configured_store raise InvalidInput, "enqueue must use the job's operation store" unless store.equal?(configured_store) now ||= RubyLLM::Browser::Rails.clock.call unless %w[inspect extract].include?(kind.to_s) raise InvalidInput, "browser jobs support inspect and extract only" end candidate = Operation.new( id: Operation.generate_id, actor_id: actor_id, run_id: run_id, budget: budget, state: :reserved, idempotency_key: idempotency_key, created_at: now, updated_at: now, request_digest: request_digest(kind: kind, url: url, instruction: instruction, schema: schema, max_bytes: max_bytes, budget: budget) ) operation = store.attach_or_reserve(candidate) perform_later( operation_id: operation.id, actor_id: actor_id, run_id: run_id, kind: kind.to_s, url: url, instruction: instruction, schema: schema, max_bytes: max_bytes ) operation end
Binds an idempotency key to the exact host-issued payload and budget.
# File lib/ruby_llm/browser/rails/operation_job.rb, line 64 def request_digest(kind:, url:, instruction:, schema:, max_bytes:, budget:) Redactor.digest(kind: kind.to_s, url: url, instruction: instruction, schema: schema, max_bytes: max_bytes, budget: budget.to_h) end
Public Instance Methods
# File lib/ruby_llm/browser/rails/operation_job.rb, line 72 def perform(operation_id:, actor_id:, run_id:, kind:, url:, instruction: nil, schema: nil, max_bytes: nil) operation = store.fetch(operation_id) return :missing if operation.nil? assert_scope!(operation, actor_id, run_id) digest = self.class.request_digest(kind: kind, url: url, instruction: instruction, schema: schema, max_bytes: max_bytes, budget: operation.budget) if operation.request_digest && operation.request_digest != digest raise NotAuthorized, "job payload does not match the reserved request" end return :cancelled if operation.state == :cancelled return :terminal if operation.terminal? return :attached unless operation.state == :reserved leased = store.lease(operation_id, actor_id: actor_id, run_id: run_id, now: now) return :cancelled if leased.state == :cancelled result = run(kind, leased, url: url, instruction: instruction, schema: schema, max_bytes: max_bytes) # Keep unknown action/start results, even when cleanup later closes a session. final = settled(operation_id) return :cancelled if final == :cancelled result.ok? ? (final || result.to_h) : result.to_h rescue InvalidInput => e raise unless fenced?(e) :fenced_out end