Skip to content

feat: count a parent's child workflows, optionally by status - #45

Merged
rgamba merged 3 commits into
mainfrom
claude/child-workflow-count-api-698f9a
Oct 27, 2026
Merged

rgamba merged 3 commits into
mainfrom
claude/child-workflow-count-api-698f9a

Conversation

@rgamba

@rgamba rgamba commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

What

A client-facing way to ask how many child workflows a parent has, optionally only those in a given set of statuses:

workflowsService.countChildWorkflows("parent-1")                          // any status
workflowsService.countChildWorkflows("parent-1", setOf(RUNNING, WAITING)) // only these

Design

  • Lives on WorkflowsService, next to countWorkflowsByStatus. It reads the store directly, so its result changes between replays. The KDoc says to call it from client code or from inside an action, never directly in a workflow method.
  • Two overloads instead of an optional set. Statuses are taken as a java.util.Set. The status overload rejects an empty set instead of reading it as "any status", so a caller whose computed set ends up empty gets an error rather than a count of every child.
  • New WorkflowStore.countChildWorkflows, implemented for MySQL and SQLite as one COUNT(*) scoped to the owner. It is a default method that throws UnsupportedOperationException, as findWorkflows does, so WorkflowStore implementations outside this repo keep compiling. The testutils tracing store delegates to it.
  • New index. parent_workflow_id had no index, so the count would have scanned every instance of the owner. This adds (owner, parent_workflow_id, status) through a MySQL migration (V7) and its SQLite counterpart (V3). It covers both query shapes, so the count never reads table rows.

Trade-offs

  • The index adds a small write cost to every workflow insert and status update, in exchange for a bounded count per parent.
  • On MySQL 5.6+ InnoDB, adding a secondary index is online and does not block writes. On a large workflow_instances table it still takes time and I/O, so deployments that apply migrations by hand should schedule V7 accordingly.

Testing

  • Shared store test (BaseWorkflowStoreTest, runs against both MySQL and SQLite). It covers status filtering, multiple statuses, a sibling parent's children, top-level workflows, the same parent ID under another owner, and a parent that doesn't exist. A second shared test points a store at a table prefix with no tables and checks that the real SQLException comes back wrapped in InternalError. Both pass on SQLite (45/45 in SqliteWorkflowStoreTest). The MySQL run happens in CI; embedded MariaDB can't install on my machine.
  • End-to-end (BaseWorkflowIntegTest). It starts two real child workflows through the factory and counts them through WorkflowsService: 2 in total, 2 COMPLETED, 0 RUNNING, and 0 for an unrelated parent. It passes.
  • Unit tests (WorkflowsServiceTest) cover both overloads and the empty-set rejection. WorkflowStoreTest covers the interface default for stores that don't override it.
  • Index use, SQLite EXPLAIN QUERY PLAN:
    SEARCH workflow_instances USING COVERING INDEX workflow_instance_index_by_parent (owner=? AND parent_workflow_id=?)
    SEARCH workflow_instances USING COVERING INDEX workflow_instance_index_by_parent (owner=? AND parent_workflow_id=? AND status=?)
    
  • Migrations. The SQLite migration applies through Flyway in the file-backed factory test. The MySQL migration is applied by the MySQL suites in CI.
  • Full local run across the root, testutils and state-machine projects: 1347 tests. The only failures are the three MySQL-backed suites, which fail during MariaDB4j setup on this machine. spotlessCheck is clean.

Add WorkflowsService.countChildWorkflows(parentWorkflowId[, statuses]),
backed by a new WorkflowStore.countChildWorkflows implemented for MySQL
and SQLite. The status overload rejects an empty list instead of reading
it as "any status".

parent_workflow_id had no index, so the count would scan every instance
of the owner. Add an (owner, parent_workflow_id, status) index through
a MySQL migration (V7) and its SQLite counterpart (V3); it covers both
query shapes.

The store method is a default that throws UnsupportedOperationException,
like findWorkflows, so out-of-tree WorkflowStore implementations keep
compiling.
Diff coverage on the PR was 68% against the 90% gate. Cover the
SQLException branch in both stores by pointing a store at a table
prefix with no tables, and cover WorkflowStore's default method
directly.

Drop the unreachable empty-result branch: COUNT(*) without GROUP BY
always yields exactly one row.
* exist).
*/
default long countChildWorkflows(
String parentWorkflowId, List<WorkflowInstance.Status> statuses) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

statuses should be a set not a list

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 3ed06fc — statuses is now a java.util.Set<WorkflowInstance.Status> (an empty set still means any status). I made the client-facing WorkflowsService.countChildWorkflows(parentWorkflowId, statusViews) overload take a Set too, so both layers agree; it still rejects an empty set.

  • Ricardo's agent


override fun countChildWorkflows(
parentWorkflowId: String,
statuses: VavrList<WorkflowInstance.Status>,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

don't use vavr type here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 3ed06fc — both the MySQL and SQLite implementations (and the testutils tracing delegate) take a plain Kotlin Set now, with no vavr types in the signature. Placeholders are built with joinToString and bound with forEachIndexed.

  • Ricardo's agent

Review feedback: statuses are a filter, so a set is the right type, and
the store signature shouldn't expose vavr. WorkflowStore,
both store implementations and the WorkflowsService overload now take
a Set; the service still rejects an empty one.
@rgamba
rgamba marked this pull request as ready for review October 26, 2026 18:59
@rgamba
rgamba merged commit 3d15454 into main Oct 27, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants