Skip to content

Release/0.7.10 - #108

Merged
Ian Streeter (istreeter) merged 3 commits into
masterfrom
release/0.7.10
Aug 11, 2026
Merged

Ian Streeter (istreeter) merged 3 commits into
masterfrom
release/0.7.10

Conversation

@istreeter

@istreeter Ian Streeter (istreeter) commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Jira ref: PDP-2827

armin-snowplow and others added 3 commits August 5, 2026 10:00
* Add snowman-tests job to cd.yml per the Snowman CI Hook Guide

Dispatches the Snowman QA framework after a tag release, waiting on the tests
only (~60-90 min); any rollout runs asynchronously on Snowman's side, so this
job is red only when tests fail.

Three deliberate deviations from the guide's example, each forced by something
specific to this repo:

  - `needs: release` rather than `needs: deploy_to_docker`. dataflow-runner
    builds and publishes its image in one job.

  - The gate is `github.repository == 'snowplow/dataflow-runner'` rather than
    `github.event.repository.private == true`. That condition exists to skip
    the public mirror of a private release repo; dataflow-runner is public-only
    with no private mirror, so it would be permanently false and the job would
    never run. Gating on the canonical repository preserves the intent — forks
    must not dispatch into Snowman — while letting the job fire here.

  - `#pipeline-rollouts` rather than `#automated-rollouts`. The latter is
    deprecated since QA-1114; pipeline and data-processing components post to
    #pipeline-rollouts, which is what the guide's own summary table says even
    though its prose still names the old channel.

The matrix is component tests only. dataflow-runner is a CLI binary invoked by
a single stack, so there is no multi-component interaction a composite would
add. The other two suites that stand up the shredder — components/rdb_loader
and composites/collector_warehouse_pipelines — do not pin
dataflow_runner_version, so dispatching a release at them would exercise the
DS4 default rather than the released version: green, and meaningless.

actionlint reports the same 6 findings before and after this change, all in the
pre-existing release job.

* Drop the release-notes step from the Snowman CI hook

The guide's example appends the Snowman run link to the GitHub release body via
softprops/action-gh-release@v2 with append_body. Removed: the same link is
already published to the Actions run summary by the preceding step, so the
release-notes copy is duplication that also gives the job write access to
releases it does not otherwise need.

The tag-type detection step stays — both summary steps still branch on it.
* Do not fail a transient run on a throttled DescribeCluster

Production runs were reported as failed after a single ThrottlingException.
None had failed: clusters had come up, or terminated cleanly, and we exited
anyway — in the launch cases leaving a cluster running its steps unwatched.
Live testing since reproduced this exactly: the old binary abandoned a cluster
that went on to reach ALL_STEPS_COMPLETED four minutes later.

The waiters swallow API errors and keep polling, so the throttled call was never
a poll. It was the describe around one: the existence check before it, or the
re-describe after it, since waiter.Wait discards its response and the status had
to be fetched again. WaitForOutput hands back the response that satisfied the
acceptor, so that second call is gone from the happy path entirely; what remains
covers only a waiter that errored or timed out, and a TERMINATED that EMR has
not yet attached a reason to.

Both transient waits lose their existence check as well. Their callers pass a
jobflow ID RunJobFlow has just returned, so it guards against nothing, and the
waiter's first poll is issued with no delay — it asks the same question and
resolves the same terminal states through its own acceptors. Dropping it removes
a duplicate call from the path where call volume is the problem, and turns a
throttled first poll into something the waiter absorbs rather than something a
retry budget has to outlast. Only `down`'s wait keeps one, since only `down`
takes a cluster ID from the operator.

Removing the job phase's check also closes a second false failure, one no
throttling was needed to reach. That check returned an already-TERMINATED
cluster with a nil error whether or not EMR had attached its reason yet, and a
reasonless TERMINATED reaches the caller's "cannot be confirmed complete"
branch. The waiter's output is now only trusted when the reason is present, and
recovered by a retried describe when it is not — and a status recovered that way
never downgrades one the launch already classified, since a bootstrap failure
often presents as plain TERMINATED and losing that code loses the relaunch.

A launch that cannot be read is left alone rather than terminated: not observing
RUNNING says nothing about whether the cluster reached it, and killing one
mid-step leaves half-written output, which is worse than an unwatched cluster
that finishes and self-terminates. Both paths that give up without a status say
which cluster they gave up on — and a wait that did see the cluster terminate
hands that status back, so nobody is sent after a cluster that has already gone.

Waiting is bounded at both ends by what could still change the answer. A
throttle, a dropped connection, a timeout or a 5xx gets the whole wait; an
answer AWS actually gave us — a role without DescribeCluster, an ID that does
not exist — ends it after three polls rather than holding the waiter for its
full bound. Waiters warn after five consecutive failed polls and every five
after that, counted in polls so the threshold means the same on a 30-90s launch
wait as on a 60-300s job one. Six hours of unbroken blindness ends the wait
outright, so the job phase cannot spend its fortnight-long bound on a cluster
that finished long ago.

The same line decides how long a call outside the waiters retries. Failures that
might clear are waited out for minutes, because these calls are how a run learns
whether it succeeded and each is issued while a cluster is already up or once it
has gone. The rest get the few seconds they always had — enough that EMR briefly
not recognising a jobflow ID it just issued is survivable, while a bad
--emr-cluster is still reported in seconds. That covers ListSteps and
DescribeStep, which sit on the transient success path and could equally turn a
completed run into a failed one, and TerminateJobFlows, which is idempotent and
whose throttling would otherwise fail `down` in three seconds.

To reduce the pressure in the first place, waiters poll at 30-90s while a
cluster launches and 60-300s during a transient job, rather than a fixed 30s:
smithy only jitters when the bounds differ, so every runner had been polling on
the same beat against a budget measured at ~18 requests of burst and ~0.8/sec
sustained for the whole account. That reduction, not the retries, is what
removes load, and the note by the budgets now points at the step-status loop as
the larger remaining source.

Both EMR clients move to adaptive retry mode, which damps a single process's own
burst rather than coordinating anything. Neither raises its attempt count, which
is passed per call instead, and only on calls that are safe to repeat: RunJobFlow
and AddJobFlowSteps share those clients and no EMR write has an idempotency
token, so retrying one whose response was merely lost would launch a second
cluster or submit the playbook's steps twice.

Three fixes to what `up` tells the operator. A successful launch no longer logs
at error level, nor fails when the waiter resolves on RUNNING rather than
WAITING. A bootstrap failure that is about to be retried is a warning, matching
the transient path, and no longer sleeps before the attempt that gives up. And
an `up` that fails after launching names the cluster it left behind — it sets
KeepJobFlowAliveWhenNoSteps, so that cluster will not terminate itself, and its
ID reached the operator nowhere else.

Also drops fetchStateChangeReason, which has had no callers since before
e9939d3 and was the last unretried DescribeCluster in the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Own the poll-again decision rather than borrowing it from the SDK

pollThroughBlindness now decides for itself that a failed poll means "ask again",
instead of deferring to the generated waiter acceptor — which returns (true, nil)
on an API error in service/emr v1.47.0 but (false, err) from v1.47.5 onward. The
acceptors are consulted only when there is a response to inspect. No behaviour
change on the pinned version.

Also reworks the unreadable-launch test to reach its branch via a cancelled
context, and corrects the blind-poll threshold comment, which implied a healthy
run never warns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wiz-2cefcfbd9e

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations 1 Low
SAST Finding SAST Findings -
Software Management Finding Software Management Findings -
Total 1 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

@istreeter
Ian Streeter (istreeter) merged commit 8dd5c02 into master Aug 11, 2026
8 of 9 checks passed
@istreeter
Ian Streeter (istreeter) deleted the release/0.7.10 branch August 11, 2026 10:50
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.

3 participants