Skip to content

Replace stringly-typed closed-set params with enums across core and SDKs #503

Description

@kartikeya-27

Summary

A sweep across crates/, sdks/python, sdks/node, and sdks/java turns up a
long tail of parameters, struct fields, and config keys that carry a closed set
of values
but are typed as bare str / String / &str. Most are guarded by a
runtime membership check (if x not in (...), VALID_ROLES.contains(...)), a few
by nothing at all — a typo is either a runtime ValueError the caller only sees in
production, or a silent no-op.

In several cases the enum already exists and the public API just doesn't use it
(DispatchOrder in core vs String order in the Java SDK; LogLevel in Python
vs String level in Java / level?: string in Node; NodeStatus in Python vs
raw string comparisons in the saga orchestrator).

This issue is an inventory + a proposal, not a single patch. It is naturally
splittable per domain.

Why bother

  • Typos become compile/type errors instead of runtime exceptions or silent
    no-ops. disabled_proxies=["requests"] today silently leaves the requests_session
    handler registered.
  • Discoverability: IDE autocomplete instead of reading a docstring for the
    allowed values.
  • Cross-SDK parity is currently invisible. Enumerating the sets immediately
    exposed three divergences (see "Parity gaps surfaced" below) that nobody would
    spot while every side is String.
  • Removes hand-rolled validators and their duplicated error-message strings.

Inventory

A. Enum exists, public API still takes a string

Value set Existing enum Raw-string surface
Dispatch order (fifo / lifo) DispatchOrdercrates/taskito-core/src/storage/mod.rs:160 sdks/python/taskito/mixins/runtime_config.py:128 (order: str, checked at :145); sdks/java/.../Taskito.java:155 + DefaultTaskito.java:221 (String order)
Task-log level LogLevelsdks/python/taskito/context.py:20 sdks/java/.../Taskito.java:334,336,344 (String level); sdks/node/src/queue.ts:1026 (level?: string); crates/taskito-core/src/storage/traits.rs:365 (level: &str)
Workflow node status NodeStatussdks/python/taskito/workflows/types.py:37 sdks/python/taskito/workflows/saga/orchestrator.py:261 and workflows/tracker/dag.py:71 compare raw strings instead of the enum
Workflow run state WorkflowState (py/java) sdks/java/.../Taskito.java:554 listWorkflowRuns(..., String state, ...); spi/QueueBackend.java:390
Interception strategy Strategysdks/python/taskito/interception/strategy.py:6 sdks/python/taskito/mixins/runtime_config.py:34 register_type(strategy: str) — coerced via S(strategy)
Workflow condition WorkflowConditionsdks/node/src/workflows/types.ts:29 sdks/java/.../workflows/Step.java:22 (String condition); sdks/python/taskito/workflows/builder.py:76,142 (condition: str | Callable | None)
Storage backend StorageBackend (storage/mod.rs:1118), Backend (storage/migrate.rs:33) sdks/python/taskito/app.py:136 (backend: str = "sqlite"); sdks/java/.../Taskito.java:604,694 (backend(String))

B. No enum anywhere — runtime-validated string

Value set Values Where
Interception mode strict / lenient / off sdks/python/taskito/interception/interceptor.py:60, validated :65; sdks/python/taskito/app.py:141
Predicate on_false defer / cancel sdks/python/taskito/mixins/decorators.py:199, validated :305; stored as dict[str, str] at mixins/predicates.py:53
Pub/sub subscriber on_error retry / skip sdks/python/taskito/mixins/pubsub.py:163, validated :186; read back at mixins/_log_consumer.py:46,95
Gate on_timeout approve / reject sdks/python/taskito/workflows/builder.py:47, validated :301 — Java already has GateAction, Python doesn't
Dashboard user role admin / viewer sdks/python/taskito/dashboard/auth.py:59,173,233; sdks/node/src/dashboard/auth/store.ts:43,152,232; sdks/java/.../dashboard/auth/AuthStore.java:33,382 — three parallel VALID_ROLES sets
Topic subscription mode log / fanout consts at crates/taskito-core/src/storage/records.rs:159,161, validated crates/taskito-core/src/pubsub.rs:16; threaded as mode: &str through storage/traits.rs:298, crates/taskito-python/src/py_queue/pubsub.rs:62,271, crates/taskito-node/src/queue/pubsub.rs:194
Webhook delivery status filter delivered / failed / dead / pending sdks/python/taskito/dashboard/handlers/webhook_deliveries.py:48

C. No enum, no validation — typo silently misbehaves

Value set Values Where Failure mode
Built-in proxy handler ids file, logger, requests_session, httpx_client, boto3_client, gcs_client sdks/python/taskito/app.py:146 (disabled_proxies: list[str] | None) consumed at proxies/built_in.py:23-75 typo → handler stays registered, silently. The example that prompted this issue.
Workflow visualize format mermaid / dot sdks/python/taskito/workflows/run.py:118, workflows/builder.py:368 (fmt: str = "mermaid", branch at :378/:132) anything that isn't "dot" silently renders Mermaid
Worker status draining, … crates/taskito-core/src/storage/traits.rs:420 update_worker_status(_, status: &str); only writer is sdks/python/taskito/mixins/lifecycle.py:200 unvalidated free text persisted to storage; no canonical set is documented anywhere
Fan-out strategy each (fan-out) / all (fan-in) sdks/python/taskito/workflows/fan_out.py:10 (strategy: str) raises ValueError at execution time, i.e. after enqueue — Java already has FanMode

Parity gaps surfaced by the sweep

Worth fixing alongside, since they only became visible by writing the sets down:

  1. ResourceScope has three different shapes.
    Python WORKER / TASK / THREAD / REQUEST (resources/definition.py:11) ·
    Java WORKER / THREAD / TASK / REQUEST / POOLED (resources/ResourceScope.java) ·
    Node "worker" | "task" | "pooled" (sdks/node/src/resources/types.ts:2).
    No two agree.
  2. DeliveryStatus: Node's union is delivered | failed | dead
    (webhooks/types.ts:41), the Python dashboard filter also accepts pending
    (dashboard/handlers/webhook_deliveries.py:48) — a value nothing appears to write.
  3. Java LogLevel (logging/LogLevel.java) is the SDK's own logger level and
    is package-private; the task-log level on the public Taskito surface is an
    unconstrained String. Two unrelated concepts, one name.

Proposal

Per-language idiom, one wire form (the current lowercase string) so nothing on the
wire or in storage changes:

  • Rustenum + serde(rename_all = "snake_case"), FromStr/as_str(). Keep
    the &str storage-trait signatures for now if a churn-free step is preferred, but
    parse at the boundary rather than string-comparing downstream.
  • PythonStrEnum (or (str, Enum), matching LogLevel/NodeStatus) and accept
    Enum | str at public entry points for one release so existing string callers keep
    working; drop the hand-rolled not in (...) checks.
  • Node — string-literal union types (already the house style; see events.ts,
    resources/types.ts) plus an exported const array for runtime validation.
  • Javaenum with a wire() / fromWire() pair, matching the existing
    FanMode / WorkflowState / GateAction pattern.

Non-goals. Open registries stay strings: user-registered proxy handler ids
(ProxyHandler.name, sdks/node/src/proxies/types.ts:31), resource names, task
names, queue names. Only the built-in proxy ids in disabled_proxies are a closed
set.

Suggested split

  1. Proxy handler ids + disabled_proxies (the silent-no-op case) — smallest, highest signal.
  2. DispatchOrder / task-log LogLevel / WorkflowState on the Java + Node public surfaces (enums already exist in core/Python).
  3. Python-side: interception mode, on_false, on_error, gate on_timeout, visualize fmt, fan-out strategy.
  4. Dashboard Role in all three SDKs — replaces three copies of VALID_ROLES.
  5. Pub/sub subscription mode + worker status in core.
  6. Reconcile the three ResourceScope definitions (needs a decision on the canonical set first).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions