feat: make bootstrapper initialiser timeouts configurable (#999)

* feat: make bootstrapper initialiser timeouts configurable

DefaultFlowStart and WorkspaceInit hardcoded the request timeouts for
their flow-svc and IAM calls, leaving operators no way to tune them for
high-latency environments (#874).

Expose them as constructor parameters threaded through the existing
initialiser `params:` mechanism, defaulting to the current values so
behaviour is unchanged unless explicitly overridden:

- DefaultFlowStart: list_timeout=10 (list-flows), start_timeout=30 (start-flow)
- WorkspaceInit: iam_timeout=10 (create-workspace)

Add unit tests for the defaults, override storage, and that configured
values reach the underlying request calls.

* test: mark async bootstrap test with @pytest.mark.asyncio

Addresses review feedback on PR #999: add the explicit
@pytest.mark.asyncio decorator to test_run_forwards_configured_timeouts
so it does not rely on asyncio_mode=auto and stays consistent with the
rest of the suite.
This commit is contained in:
corvus-0x 2026-06-30 04:37:22 -04:00 committed by GitHub
parent 5cb4f83afa
commit 1aa9549912
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 85 additions and 5 deletions

View file

@ -0,0 +1,54 @@
"""
Unit tests for trustgraph.bootstrap.initialisers.DefaultFlowStart
Verifies the list/start timeouts are configurable and that the
configured values actually reach the flow-client request calls.
"""
from unittest.mock import AsyncMock, MagicMock
import pytest
from trustgraph.bootstrap.initialisers.default_flow_start import (
DefaultFlowStart,
)
def test_default_timeouts():
init = DefaultFlowStart(blueprint="bp")
assert init.list_timeout == 10
assert init.start_timeout == 30
def test_timeout_overrides_are_stored():
init = DefaultFlowStart(blueprint="bp", list_timeout=5, start_timeout=99)
assert init.list_timeout == 5
assert init.start_timeout == 99
@pytest.mark.asyncio
async def test_run_forwards_configured_timeouts():
init = DefaultFlowStart(blueprint="bp", list_timeout=5, start_timeout=99)
# Flow client: list-flows returns no error + empty flow list,
# start-flow returns no error.
flow = MagicMock()
flow.start = AsyncMock()
flow.stop = AsyncMock()
flow.request = AsyncMock(side_effect=[
MagicMock(error=None, flow_ids=[]), # list-flows response
MagicMock(error=None), # start-flow response
])
# Context: workspace "default" exists, hands back our mock flow client.
ctx = MagicMock()
ctx.logger = MagicMock()
ctx.config.keys = AsyncMock(return_value=["default"])
ctx.make_flow_client = MagicMock(return_value=flow)
await init.run(ctx, None, "v1")
calls = flow.request.call_args_list
assert len(calls) == 2
assert calls[0].kwargs["timeout"] == 5
assert calls[1].kwargs["timeout"] == 99

View file

@ -0,0 +1,13 @@
"""Unit tests for trustgraph.bootstrap.initialisers.WorkspaceInit."""
from trustgraph.bootstrap.initialisers.workspace_init import WorkspaceInit
def test_default_iam_timeout():
init = WorkspaceInit()
assert init.iam_timeout == 10
def test_iam_timeout_override_is_stored():
init = WorkspaceInit(iam_timeout=42)
assert init.iam_timeout == 42

View file

@ -18,6 +18,10 @@ description : str (default "Default")
Human-readable description passed to flow-svc.
parameters : dict (optional)
Optional parameter overrides passed to start-flow.
list_timeout : int (default 10)
Timeout in seconds for the list-flows request.
start_timeout : int (default 30)
Timeout in seconds for the start-flow request.
"""
from trustgraph.schema import FlowRequest
@ -34,6 +38,8 @@ class DefaultFlowStart(Initialiser):
blueprint=None,
description="Default",
parameters=None,
list_timeout=10,
start_timeout=30,
**kwargs,
):
super().__init__(**kwargs)
@ -46,6 +52,8 @@ class DefaultFlowStart(Initialiser):
self.blueprint = blueprint
self.description = description
self.parameters = dict(parameters) if parameters else {}
self.list_timeout = list_timeout
self.start_timeout = start_timeout
async def run(self, ctx, old_flag, new_flag):
@ -70,7 +78,7 @@ class DefaultFlowStart(Initialiser):
FlowRequest(
operation="list-flows",
),
timeout=10,
timeout=self.list_timeout,
)
if list_resp.error:
raise RuntimeError(
@ -99,7 +107,7 @@ class DefaultFlowStart(Initialiser):
description=self.description,
parameters=self.parameters,
),
timeout=30,
timeout=self.start_timeout,
)
if resp.error:
raise RuntimeError(

View file

@ -14,7 +14,9 @@ seed_file : str (required when source=="seed-file")
Path to a JSON seed file with the same shape TemplateSeed consumes.
overwrite : bool (default False)
On re-run (flag change), if True overwrite all keys; if False,
upsert-missing-only (preserves in-workspace customisations).
upsert-missing-only (preserves in-workspace customisations)
iam_timeout : int (default 10)
Timeout in seconds for the IAM create-workspace request.
Raises (in ``run``)
-------------------
@ -41,7 +43,9 @@ class WorkspaceInit(Initialiser):
source="template",
seed_file=None,
overwrite=False,
iam_timeout=10,
**kwargs,
):
super().__init__(**kwargs)
@ -59,6 +63,7 @@ class WorkspaceInit(Initialiser):
self.source = source
self.seed_file = seed_file
self.overwrite = overwrite
self.iam_timeout = iam_timeout
async def run(self, ctx, old_flag, new_flag):
await self._create_workspace(ctx)
@ -120,10 +125,10 @@ class WorkspaceInit(Initialiser):
workspace_record=WorkspaceInput(
id=self.workspace,
name=self.workspace.title(),
enabled=True,
enabled=True,
),
),
timeout=10,
timeout=self.iam_timeout,
)
if resp.error:
if resp.error.type == "duplicate":