Class: SolidLoop::ToolMiddlewares::ToolExecution

Inherits:
Object
  • Object
show all
Defined in:
app/services/solid_loop/tool_middlewares/tool_execution.rb

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ ToolExecution

Returns a new instance of ToolExecution.



4
5
6
# File 'app/services/solid_loop/tool_middlewares/tool_execution.rb', line 4

def initialize(app)
  @app = app
end

Instance Method Details

#call(env) ⇒ Object

LOCK ORDER (the tool canonical-write fence): loop row → tool_call → message, deterministic ascending id where several rows of one kind are locked. ToolExecution takes the first two rungs (loop, tool_call); ResponseCreation continues onto the message rung under the SAME loop lock. The both-token validation AND every canonical write are held under the LOOP row lock, so a pause/resume/stop (which only takes the loop lock) cannot slip between validation and the write: with the loop FOR UPDATE-locked, loop.execution_token and tool_call.lease_token cannot change between validation and mutation, so a stale/revoked worker writes NOTHING.

CRITICAL: the tool BODY runs OUTSIDE the loop lock. A long-running tool holding the loop lock would serialize pause/stop/resume behind it — the very thing the fence exists to make safe. The side effect (at-least-once) may run even for a worker that ultimately loses the fence — that discarded effect is the documented at-least-once boundary (the stable ToolCall key dedupes). Only the validate-then-write step is locked.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'app/services/solid_loop/tool_middlewares/tool_execution.rb', line 24

def call(env)
  # Terminal (executed_at) OR already has a canonical response (a crash
  # between the response commit and the executed_at checkpoint): the tool
  # BODY must NOT re-run. Take the fenced backfill+restore path only.
  if env.tool_call.executed_at? || env.tool_call.response_message
    resolve_from_checkpoint(env)
    return @app.call(env)
  end

  # Run the tool body WITHOUT any loop lock held (a long tool must never
  # serialize pause/stop/resume behind the loop lock). The side effect is
  # at-least-once; the canonical write below is what the fence guards.
  execute_once(env)

  # Test-only failpoint (no-op in production): runs AFTER the tool body and
  # BEFORE the loop lock is acquired, so a concurrency spec can commit a
  # racing pause/stop in exactly this window and prove the subsequent
  # under-lock validation fences the now-stale worker out.
  env.[:before_lock_acquire]&.call

  env.loop.with_lock do
    env.tool_call.with_lock do
      # Re-check under the fence: a concurrent worker may have written the
      # response/checkpoint while our body ran. If so, restore instead of
      # writing a second canonical result.
      backfill_checkpoint_from_response(env)

      if env.tool_call.executed_at?
        restore_checkpoint(env)
        next
      end

      # The canonical `executed_at` + result write
      # is fenced with BOTH tokens while the LOOP row is still locked, so a
      # worker whose per-tool lease was revoked (pause/reclaim) or whose
      # generation was rotated away writes NOTHING canonical. `executed_at`
      # stays the terminal exactly-once guard.
      unless canonical_write_owned?(env)
        env.[:tool_write_fenced_out] = true
        return
      end

      # Test-only failpoint (no-op in production): a callable placed here
      # runs INSIDE the loop lock, AFTER the both-token validation and
      # BEFORE the canonical write, so a concurrency spec can attempt a
      # racing pause/stop at exactly that interleaving and prove the loop
      # row lock serializes it. Harmless when absent.
      env.[:after_validate_before_write]&.call

      env.tool_call.update!({
        is_success: env.is_success,
        result: env.result,
        duration: env.duration || 0.0,
        executed_at: Time.current,
        lease_token: nil,
        leased_until: nil
      }.merge(env..key?(:mcp_session_id) ? { mcp_session_id: env.[:mcp_session_id] } : {}))
      env.loop.increment!(:duration_tools, env.duration) if env.duration.to_f.nonzero?
    end
  end

  @app.call(env)
end