Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(robot-server): Use last run command for /runs/:runId/currentState links #16557

Merged
merged 5 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api-client/src/runs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export interface RunsLinks {
}

export interface RunCommandLink {
current: CommandLinkNoMeta
last: CommandLinkNoMeta
}

export interface CommandLinkNoMeta {
Expand Down
16 changes: 16 additions & 0 deletions api/src/opentrons/protocol_runner/run_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,22 @@ def get_current_command(self) -> Optional[CommandPointer]:
"""Get the "current" command, if any."""
return self._protocol_engine.state_view.commands.get_current()

def get_most_recently_finalized_command(self) -> Optional[CommandPointer]:
"""Get the most recently finalized command, if any."""
most_recently_finalized_command = (
self._protocol_engine.state_view.commands.get_most_recently_finalized_command()
)
return (
CommandPointer(
command_id=most_recently_finalized_command.command.id,
command_key=most_recently_finalized_command.command.key,
created_at=most_recently_finalized_command.command.createdAt,
index=most_recently_finalized_command.index,
)
if most_recently_finalized_command
else None
)

def get_command_slice(
self, cursor: Optional[int], length: int, include_fixit_commands: bool
) -> CommandSlice:
Expand Down
19 changes: 10 additions & 9 deletions robot-server/robot_server/runs/router/base_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ class AllRunsLinks(BaseModel):
class CurrentStateLinks(BaseModel):
"""Links returned with the current state of a run."""

current: Optional[CommandLinkNoMeta] = Field(
last: Optional[CommandLinkNoMeta] = Field(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider naming this lastCompleted instead of just last, in order to avoid confusion with the last queued command, which hasn't necessarily executed yet, and so is a different thing.

None,
description="Path to the current command when current state was reported, if any.",
description="Path to the last completed command when current state was reported, if any.",
)


Expand Down Expand Up @@ -564,7 +564,7 @@ async def get_run_commands_error(
"""
),
responses={
status.HTTP_200_OK: {"model": SimpleBody[RunCurrentState]},
status.HTTP_200_OK: {"model": Body[RunCurrentState, CurrentStateLinks]},
status.HTTP_409_CONFLICT: {"model": ErrorBody[RunStopped]},
},
)
Expand All @@ -590,17 +590,18 @@ async def get_current_state(
for pipetteId, nozzle_map in active_nozzle_maps.items()
}

current_command = run_data_manager.get_current_command(run_id=runId)
last_completed_command = run_data_manager.get_last_completed_command(
run_id=runId
)
except RunNotCurrentError as e:
raise RunStopped(detail=str(e)).as_error(status.HTTP_409_CONFLICT)

# TODO(jh, 03-11-24): Use `last_completed_command` instead of `current_command` to avoid concurrency gotchas.
links = CurrentStateLinks.construct(
current=CommandLinkNoMeta.construct(
id=current_command.command_id,
href=f"/runs/{runId}/commands/{current_command.command_id}",
last=CommandLinkNoMeta.construct(
id=last_completed_command.command_id,
href=f"/runs/{runId}/commands/{last_completed_command.command_id}",
)
if current_command is not None
if last_completed_command is not None
else None
)

Expand Down
37 changes: 33 additions & 4 deletions robot-server/robot_server/runs/run_data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,10 +456,20 @@ def get_current_command(self, run_id: str) -> Optional[CommandPointer]:
if self._run_orchestrator_store.current_run_id == run_id:
return self._run_orchestrator_store.get_current_command()
else:
# todo(mm, 2024-05-20):
# For historical runs to behave consistently with the current run,
# this should be the most recently completed command, not `None`.
return None
return self._get_historical_run_last_command(run_id=run_id)

def get_last_completed_command(self, run_id: str) -> Optional[CommandPointer]:
"""Get the "last" command, if any.

See `ProtocolEngine.state_view.commands.get_most_recently_finalized_command()` for the definition of "last."

Args:
run_id: ID of the run.
"""
if self._run_orchestrator_store.current_run_id == run_id:
return self._run_orchestrator_store.get_most_recently_finalized_command()
else:
return self._get_historical_run_last_command(run_id=run_id)

def get_recovery_target_command(self, run_id: str) -> Optional[CommandPointer]:
"""Get the current error recovery target.
Expand Down Expand Up @@ -554,3 +564,22 @@ def _get_run_time_parameters(self, run_id: str) -> List[RunTimeParameter]:
return self._run_orchestrator_store.get_run_time_parameters()
else:
return self._run_store.get_run_time_parameters(run_id=run_id)

def _get_historical_run_last_command(self, run_id: str) -> Optional[CommandPointer]:
command_slice = self._run_store.get_commands_slice(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up that for older historical JSON runs, which enqueued all of their commands up-front instead of one-by-one as the run proceeded, this will return the last command in the whole protocol, not the last command that actually ran.

I think this is close enough that we shouldn't worry about it.

run_id=run_id, cursor=None, length=1, include_fixit_commands=True
)
if not command_slice.commands:
return None
command = command_slice.commands[-1]

return (
CommandPointer(
command_id=command.id,
command_key=command.key,
created_at=command.createdAt,
index=command_slice.total_length - 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be equivalent, but you might find it clearer to do index=command_slice.cursor instead of index=command_slice.total_length - 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh yep, that is cleaner, thanks

)
if command
else None
)
6 changes: 5 additions & 1 deletion robot-server/robot_server/runs/run_orchestrator_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,13 @@ def get_run_time_parameters(self) -> List[RunTimeParameter]:
return self.run_orchestrator.get_run_time_parameters()

def get_current_command(self) -> Optional[CommandPointer]:
"""Get the current running command."""
"""Get the current running command, if any."""
return self.run_orchestrator.get_current_command()

def get_most_recently_finalized_command(self) -> Optional[CommandPointer]:
"""Get the most recently finalized command, if any."""
return self.run_orchestrator.get_most_recently_finalized_command()

def get_command_slice(
self, cursor: Optional[int], length: int, include_fixit_commands: bool
) -> CommandSlice:
Expand Down
14 changes: 8 additions & 6 deletions robot-server/tests/runs/router/test_base_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -876,10 +876,12 @@ async def test_get_current_state_success(
decoy.when(mock_run_data_manager.get_nozzle_maps(run_id=run_id)).then_return(
mock_nozzle_maps
)
decoy.when(mock_run_data_manager.get_current_command(run_id=run_id)).then_return(
decoy.when(
mock_run_data_manager.get_last_completed_command(run_id=run_id)
).then_return(
CommandPointer(
command_id="current-command-id",
command_key="current-command-key",
command_id="last-command-id",
command_key="last-command-key",
created_at=datetime(year=2024, month=4, day=4),
index=101,
)
Expand All @@ -901,9 +903,9 @@ async def test_get_current_state_success(
}
)
assert result.content.links == CurrentStateLinks(
current=CommandLinkNoMeta(
href="/runs/test-run-id/commands/current-command-id",
id="current-command-id",
last=CommandLinkNoMeta(
href="/runs/test-run-id/commands/last-command-id",
id="last-command-id",
)
)

Expand Down
97 changes: 95 additions & 2 deletions robot-server/tests/runs/test_run_data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,12 +1004,105 @@ def test_get_current_command_not_current_run(
subject: RunDataManager,
mock_run_store: RunStore,
mock_run_orchestrator_store: RunOrchestratorStore,
run_command: commands.Command,
) -> None:
"""Should return None because the run is not current."""
"""Should get the last command from the run store for a historical run."""
last_command_slice = commands.WaitForResume(
id="command-id-1",
key="command-key",
createdAt=datetime(year=2021, month=1, day=1),
status=commands.CommandStatus.SUCCEEDED,
params=commands.WaitForResumeParams(message="Hello"),
)

expected_last_command = CommandPointer(
command_id="command-id-1",
command_key="command-key",
created_at=datetime(year=2021, month=1, day=1),
index=0,
)

command_slice = CommandSlice(
commands=[last_command_slice], cursor=1, total_length=1
)

decoy.when(mock_run_orchestrator_store.current_run_id).then_return("not-run-id")
decoy.when(
mock_run_store.get_commands_slice(
run_id="run-id", cursor=None, length=1, include_fixit_commands=True
)
).then_return(command_slice)
result = subject.get_current_command("run-id")

assert result is None
assert result == expected_last_command


def test_get_last_completed_command_current_run(
decoy: Decoy,
subject: RunDataManager,
mock_run_orchestrator_store: RunOrchestratorStore,
run_command: commands.Command,
) -> None:
"""Should get the last command from the engine store for the current run."""
run_id = "current-run-id"
expected_last_command = CommandPointer(
command_id=run_command.id,
command_key=run_command.key,
created_at=run_command.createdAt,
index=5,
)

decoy.when(mock_run_orchestrator_store.current_run_id).then_return(run_id)
decoy.when(
mock_run_orchestrator_store.get_most_recently_finalized_command()
).then_return(expected_last_command)

result = subject.get_last_completed_command(run_id)

assert result == expected_last_command


def test_get_last_completed_command_not_current_run(
decoy: Decoy,
subject: RunDataManager,
mock_run_orchestrator_store: RunOrchestratorStore,
mock_run_store: RunStore,
run_command: commands.Command,
) -> None:
"""Should get the last command from the run store for a historical run."""
run_id = "historical-run-id"

last_command_slice = commands.WaitForResume(
id="command-id-1",
key="command-key",
createdAt=datetime(year=2021, month=1, day=1),
status=commands.CommandStatus.SUCCEEDED,
params=commands.WaitForResumeParams(message="Hello"),
)

expected_last_command = CommandPointer(
command_id="command-id-1",
command_key="command-key",
created_at=datetime(year=2021, month=1, day=1),
index=0,
)

decoy.when(mock_run_orchestrator_store.current_run_id).then_return(
"different-run-id"
)

command_slice = CommandSlice(
commands=[last_command_slice], cursor=1, total_length=1
)
decoy.when(
mock_run_store.get_commands_slice(
run_id=run_id, cursor=None, length=1, include_fixit_commands=True
)
).then_return(command_slice)

result = subject.get_last_completed_command(run_id)

assert result == expected_last_command


def test_get_command_from_engine(
Expand Down
Loading