Skip to content

fix(microvm-worker): wait at least 30 s after a suspend refused for now - #799

Draft
ParidelPooya wants to merge 7 commits into
mainfrom
fix/microvm-worker-refused-for-now-floor
Draft

ParidelPooya wants to merge 7 commits into
mainfrom
fix/microvm-worker-refused-for-now-floor

Conversation

@ParidelPooya

Copy link
Copy Markdown
Contributor

Fixes #797.

Summary

After a suspend that the service refused for now, the MicroVM worker now waits at least 30 seconds before its next SuspendMicrovm call. The docs now say which errors reach "refused for now" with the default client. The JavaScript worker made the same change in aws/aws-durable-execution-sdk-js#1040.

Why

  1. feat(microvm-worker): act on lifecycle hooks #782 added the worker's own suspend when idle. A first-attempt error that the service answers before it acts is "refused for now". The worker then accepts jobs again at once and starts the idle time again.
  2. Some of these errors repeat on every call. A wrong secret key gives a signature error each time. A static session token gives ExpiredTokenException each time after it expires.
  3. The run hook accepts any positive autoSuspendIdleSeconds. So an idle MicroVM made one call per idle time, with no lower bound, and wrote two log lines per call.
  4. A probe with an idle time of 0.0001 s counted 15 calls in 3 s of real time. The count depends on the machine.
  5. The docs said that a first-attempt throttle is the usual case for "refused for now". The default client uses botocore's standard retry mode with 3 attempts, and that mode retries ThrottlingException. A probe showed that a 429 on every attempt ends with RetryAttempts 2. So the error is classified unknown.
  6. botocore retries no signature, clock-skew or expired-token error. So those errors stay "refused for now" with the default client.

Changes

The 30-second minimum

  1. _start_idle_time(after_refusal=True) uses max(autoSuspendIdleSeconds, 30 s) for the next idle time.
  2. The refused-for-now branch runs while the call is still counted in _suspend_calls. So the request waits in _idle_after_call, and _idle_after_call_floor records the minimum. _suspend_call_ended applies it.
  3. A later request during the same call overwrites the recorded minimum. So the latest request decides.
  4. A resume hook or a job that ends during the call does not shorten the wait, because the refused outcome arrives after it.
  5. A job or a resume hook after the refusal starts the normal idle time.
  6. While no job ends and no lifecycle hook arrives, the worker makes at most one call per 30 seconds.

Warnings

The refused-for-now warning now has two texts. The worker chooses the text under the lock, from the same decision that sets the minimum.

  • Without a suspend hook during the call: the worker tries again after the next idle time. That idle time lasts at least 30 seconds, unless a job or a resume hook starts the idle time again.
  • With a suspend hook during the call: the hook's refusal of jobs stays. It ends when the resume hook arrives, or 30 seconds after the suspend hook. The idle time then starts again.

Docs

  • README item 7 and the docstrings of SuspendOutcome, classify_suspend_error, create_microvms_client and the listener say that the default client retries a throttle. So a throttle usually ends as unknown. "Refused for now" covers expired credentials and a signature error, with or without a clock offset. A throttle counts only from a client that does not retry it.
  • The JavaScript default client also retries clock-skew errors. So there, a clock-skew error usually ends as unknown. README item 7 states this difference.
  • The create_microvms_client option doc says that a caller client that retries must report its retries in ResponseMetadata.RetryAttempts on each ClientError. Otherwise a retried throttle counts as a first attempt.

Testing

  • Package: 411 tests pass. worker.py and self_suspend.py are at 100 % line and branch coverage. Typecheck, hatch fmt --check and the commit lint pass.
  • New tests with fake timers:
    • the minimum at idle times of 1 s, 10 s and 45 s;
    • one call per 30 s while the refusal lasts;
    • the normal idle time after a job or a resume hook that follows the refusal;
    • the minimum after a resume hook, or a job that starts and ends, during the call;
    • the normal idle time after a job that runs across the refused outcome;
    • both warning texts.
  • A test with the real default client checks the retry count and the outcome for a throttle, a skewed signature error, a signature mismatch and an expired token.
  • Six mutations of the minimum each fail at least one test. Examples: no minimum, min instead of max, the minimum on every start, the first request during the call deciding instead of the latest.
  • A loop probe over 3 s of real time made 1 call for every idle time and error after the change. Before the change it made up to 15.
  • The package suite passed 3 of 3 runs with 8 parallel workers.
  • Three rounds of review. The review docs are not part of the PR.
  • Not run against the live service.

The docs said that a first-attempt throttle or a clock offset is the
normal case for a suspend refused for now. A probe of the default
client showed otherwise.

1. The default client uses botocore's standard retry mode with 3
   attempts.
2. That mode retries ThrottlingException. A 429 on every attempt ends
   with RetryAttempts 2.
3. An error after a retry is an unknown outcome. So a throttle from the
   default client usually ends as unknown, with the 30-second refusal.
4. botocore retries no signature or token error, also when the server's
   Date header is 10 minutes ahead. These errors end with RetryAttempts
   0, so they stay refused for now.

The README and the docstrings now state this. The create_microvms_client
doc states that a client that retries must report RetryAttempts on its
error. Without it, a retried throttle counts as a first attempt.

A new test pins the default client's retries for these four errors.
After a suspend refused for now, the worker started the idle time
again at once. Some of these errors repeat on every call. A wrong secret
key gives a signature error on every call. A static session token gives
ExpiredTokenException on every call after it expires. So an idle
MicroVM made one SuspendMicrovm call per idle time, without a bound.
With a 0.0001-second idle time, a probe counted 15 calls and 30 log
lines in 3 seconds.

The restart after a refusal for now now waits max(idle time, 30 s).
The 30 s is SUSPEND_GRACE_SECONDS. The probe now counts 1 call in 3
seconds for each idle time.

1. The refused branch asks for the idle time with after_refusal=True.
2. The call is still counted as running at that point. So the request
   waits in _idle_after_call, and _idle_after_call_floor records the
   floor.
3. Each later request during the same call overwrites the floor. So
   the latest request decides.
4. A resume hook during the call comes before the refused outcome. So
   the floor applies. A job that ends after the outcome writes False.
   So the normal idle time applies.
5. Every other start, such as a job end or a resume hook after the
   refusal, uses the normal idle time.

The warning is still logged once per refused call. The JavaScript
worker (#1040) is being changed to the same rule.
The warning for a suspend refused for now said that the worker tries
again after the next idle time, "at least 30 seconds". That promise was
false in two cases.

1. A suspend hook can arrive during the call. The hook's refusal of
   jobs then stays, and the worker sets no 30-second floor. The worker
   still logged the same warning.
2. A job that ends after the refusal, or a resume hook after it, starts
   the normal idle time. So the next call can come sooner than 30 s.

The worker now checks the suspend hook count first, and then logs one
of two warnings.

- Without a suspend hook during the call, the warning says that the
  next idle time lasts at least 30 seconds, unless a job or a resume
  hook starts the idle time again.
- With a suspend hook during the call, the warning says that the
  hook's refusal stays. That refusal ends when the resume hook arrives,
  or 30 seconds after the suspend hook. The idle time then starts
  again.

The tests now assert both texts in full. The JavaScript worker (#1040)
is being changed to the same wording.
Two paths had no test. A probe in review showed the right behavior for
both.

1. A resume hook arrives during the call, and a job starts. The call
   ends refused for now while the job runs. The job ends after the
   call. The worker makes no call while the job runs. It makes one
   call one normal idle time after the job ends.
2. A resume hook arrives during the call. A job starts and ends during
   the call. The call then ends refused for now. The refused outcome is
   the later request to start the idle time. So the 30-second floor
   applies.
1. README item 7 said that the JavaScript worker decides these cases
   by HTTP status. The JavaScript worker checks the error name first,
   as this worker does. The two differ in the default client. The
   JavaScript client retries a clock-skew error. So there, a clock-skew
   error usually ends as an unknown outcome.
2. README item 7 said that the worker makes at most one call per 30
   seconds while the error lasts. A job that ends restarts the normal
   idle time. So the limit holds only while no job ends and no
   lifecycle hook arrives. The sentence now states that condition.
3. README item 7 said that only a resume hook during the call keeps
   the floor. A job that ends during the call keeps it too, because the
   refused outcome arrives after the job's end. The sentence now names
   both.
4. The create_microvms_client docstring said that each error reports
   its retries in ResponseMetadata.RetryAttempts. Only a ClientError
   carries ResponseMetadata. The docstring now says ClientError.
Item 7 of the listener docstring named only a resume hook during the
refused call. The README also names a job that ends during the call.
Both keep the 30-second wait, because the refused outcome arrives after
them. So the docstring now names both.

This branch has not been deployed

No deployments
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.

MicroVM worker: wait at least 30 seconds after a suspend refused for now

1 participant