Skip to content

StepFunctions parser: second StartExecution on the same state machine fails with 'cannot pickle _thread.RLock' (deepcopy of executions) #10077

Description

@tinovyatkin

Summary

Under the Step Functions parser backend (stepfunctions.execute_state_machine=True), starting the same state machine a second time fails with a 500 / TypeError: cannot pickle '_thread.RLock' object.

StepFunctionsParserBackend.start_execution does state_machine_clone = copy.deepcopy(state_machine) (moto/stepfunctions/parser/models.py#L175). After the first execution, state_machine.executions contains an Execution whose exec_worker is a live ExecutionWorker (a threading.Thread plus an Environment holding a _thread.RLock). deepcopy traverses the executions list and tries to copy that lock, which is unpicklable.

This also makes the arn:aws:states:::states:startExecution.sync[:2] service integration unusable (a parent SM starting a child SM triggers a second start_execution on the backend), but the bug is more general — it happens for any second StartExecution on a state machine that has already run.

How to reproduce

import json, time, os
import boto3
from moto.server import ThreadedMotoServer
from moto.core.config import default_user_config

default_user_config["stepfunctions"]["execute_state_machine"] = True
os.environ["MOTO_PORT"] = "5000"
srv = ThreadedMotoServer(port=5000); srv.start()
sfn = boto3.client("stepfunctions", endpoint_url="http://localhost:5000",
                   region_name="us-east-1", aws_access_key_id="x", aws_secret_access_key="x")

arn = sfn.create_state_machine(
    name="sm", roleArn="arn:aws:iam::123456789012:role/sfn",
    definition=json.dumps({"StartAt": "P", "States": {"P": {"Type": "Pass", "End": True}}}),
)["stateMachineArn"]

e1 = sfn.start_execution(stateMachineArn=arn, name="run1", input="{}")["executionArn"]
time.sleep(1)
print("run1:", sfn.describe_execution(executionArn=e1)["status"])   # SUCCEEDED

# Second execution of the SAME state machine:
e2 = sfn.start_execution(stateMachineArn=arn, name="run2", input="{}")["executionArn"]  # 500
time.sleep(1)
print("run2:", sfn.describe_execution(executionArn=e2)["status"])
srv.stop()

Expected

run2 reaches SUCCEEDED, same as run1.

Actual

run1: SUCCEEDED
run2 -> ClientError: An error occurred (500) when calling the StartExecution operation

Server-side traceback:

File ".../moto/stepfunctions/parser/models.py", line 175, in start_execution
    state_machine_clone = copy.deepcopy(state_machine)
  ...
  File ".../copy.py", line 151, in deepcopy
    rv = reductor(4)
TypeError: cannot pickle '_thread.RLock' object

Notes / possible fix

The intent of the deepcopy (per the comment just above it) is to snapshot the state machine definition so per-execution mutations don't leak back. But state_machine.executions (which holds live worker threads/locks) shouldn't be part of that clone. Options:

  • Exclude executions (and the live worker/env state) from the copy — e.g. a __deepcopy__ that copies definition fields but not the runtime executions, or temporarily detach executions around the copy.
  • Snapshot only the definition/config rather than the whole backend object.

Likely related to the LocalStack parser port (#9142). Sibling issue (same port, different symptom): #10076 (PascalCase ASL params not converted to boto member casing for start_execution).

Environment

  • moto 5.1.22
  • boto3 1.42.93 / botocore 1.42.93
  • Python 3.12.13

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions