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
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_executiondoesstate_machine_clone = copy.deepcopy(state_machine)(moto/stepfunctions/parser/models.py#L175). After the first execution,state_machine.executionscontains anExecutionwhoseexec_workeris a liveExecutionWorker(athreading.Threadplus anEnvironmentholding a_thread.RLock).deepcopytraverses theexecutionslist 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 secondstart_executionon the backend), but the bug is more general — it happens for any secondStartExecutionon a state machine that has already run.How to reproduce
Expected
run2reachesSUCCEEDED, same asrun1.Actual
Server-side traceback:
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. Butstate_machine.executions(which holds live worker threads/locks) shouldn't be part of that clone. Options:executions(and the live worker/env state) from the copy — e.g. a__deepcopy__that copies definition fields but not the runtimeexecutions, or temporarily detachexecutionsaround the copy.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