Skip to content

bug: shared OperationStepExecutor across AggregateFunction instances causes race condition on exposeNamespace #320

@eskenazit

Description

@eskenazit

Component

Core Engine

Description

Actual behavior:
Capability creates a single shared OperationStepExecutor instance for all aggregate
functions across all aggregates:

OperationStepExecutor sharedExecutor = new OperationStepExecutor(this);
for (AggregateSpec aggSpec : spec.getCapability().getAggregates()) {
    this.aggregates.add(new Aggregate(aggSpec, sharedExecutor));
}

AggregateFunction.execute() calls stepExecutor.setExposeNamespace(namespace) immediately
before executeSteps(). Because the executor is shared, two concurrent requests targeting
functions from aggregates with different namespaces (e.g. shipyard and logistics) can
interleave:

  • Thread A: setExposeNamespace("shipyard")
  • Thread B: setExposeNamespace("logistics")
  • Thread A: executeSteps(...) reads exposeNamespace → gets "logistics" → wrong
    namespace-qualified references resolved

The volatile keyword guarantees visibility but not atomicity of the set+use pair.

Expected behavior:
Each AggregateFunction execution should resolve namespace-qualified references using its
own aggregate's namespace, regardless of concurrent calls to other functions.

Root Cause

Capability shares one OperationStepExecutor across all AggregateFunction instances.
exposeNamespace is a mutable volatile field set just before each execution, making the
set+use sequence non-atomic and subject to a TOCTOU race under concurrency.

By contrast, ToolHandler, ResourceHandler, and ResourceRestlet each own a dedicated
executor with exposeNamespace set once at construction time — they are not affected.

Suggested Fix

Give each AggregateFunction its own OperationStepExecutor instance (constructed in
Aggregate rather than in Capability), so exposeNamespace can be set immutably at
construction time. This aligns the aggregate execution model with the handler execution model.

Steps to Reproduce

  1. Define a capability with two aggregates of different namespaces, each with an orchestrated function that has a step-level with using namespace-qualified references
  2. Send concurrent requests to both functions (e.g. via two simultaneous MCP tool calls)
  3. Observe that one or both calls resolve with values against the wrong namespace, producing
    incorrect HTTP request parameters

Capability File (if relevant)

naftiko: "1.0.0-alpha2"
info:
  label: "Shared Executor Namespace Race Condition"
  description: "Reproduces concurrent namespace pollution on the shared OperationStepExecutor"
  tags:
    - Test
    - Aggregate
    - Concurrency
  created: "2026-04-14"
  modified: "2026-04-14"

capability:
  aggregates:
    - label: "Shipyard"
      namespace: "shipyard"
      functions:
        - name: "get-voyage-manifest"
          description: "Assemble a voyage manifest."
          inputParameters:
            - name: "voyage-id"
              type: "string"
              description: "Voyage identifier"
          steps:
            - name: "fetch-voyage"
              type: call
              call: "registry.get-voyage"
              with:
                voyage-id: "shipyard.voyage-id"   # must resolve to caller's voyage-id
          mappings:
            - targetName: "voyage-id"
              value: "$.fetch-voyage.voyageId"
          outputParameters:
            - name: "voyage-id"
              type: "string"

    - label: "Logistics"
      namespace: "logistics"
      functions:
        - name: "get-shipment-status"
          description: "Retrieve the status of a shipment."
          inputParameters:
            - name: "shipment-id"
              type: "string"
              description: "Shipment identifier"
          steps:
            - name: "fetch-shipment"
              type: call
              call: "warehouse.get-shipment"
              with:
                shipment-id: "logistics.shipment-id"   # must resolve to caller's shipment-id
          mappings:
            - targetName: "shipment-id"
              value: "$.fetch-shipment.shipmentId"
          outputParameters:
            - name: "shipment-id"
              type: "string"

  exposes:
    - type: "mcp"
      address: "localhost"
      port: 9300
      namespace: "fleet-mcp"
      description: "MCP server exposing both aggregate functions."
      tools:
        - name: "get-voyage-manifest"
          description: "Get the voyage manifest."
          ref: "shipyard.get-voyage-manifest"
        - name: "get-shipment-status"
          description: "Get the shipment status."
          ref: "logistics.get-shipment-status"

  consumes:
    - type: "http"
      namespace: "registry"
      baseUri: "http://localhost:19999"
      resources:
        - name: "voyage"
          path: "/voyages/{{voyage-id}}"
          operations:
            - name: "get-voyage"
              method: GET
              inputParameters:
                - name: "voyage-id"
                  in: path

    - type: "http"
      namespace: "warehouse"
      baseUri: "http://localhost:19998"
      resources:
        - name: "shipment"
          path: "/shipments/{{shipment-id}}"
          operations:
            - name: "get-shipment"
              method: GET
              inputParameters:
                - name: "shipment-id"
                  in: path

Logs & Stacktrace


Version

1.0.0-alpha2

Runtime

JVM

Agent Context (optional)

agent_name: GitHub Copilot
llm: Claude Sonnet 4.6
tool: VS Code Chat
confidence: high
source_event: pr_review_317
discovery_method: code_review
files_suspected:
  - src/main/java/io/naftiko/Capability.java
  - src/main/java/io/naftiko/engine/aggregates/Aggregate.java
  - src/main/java/io/naftiko/engine/aggregates/AggregateFunction.java
  - src/main/java/io/naftiko/engine/exposes/OperationStepExecutor.java

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions