Skip to content

Commit

Permalink
Standardize to handle durations as Temporal.Duration in every inner i…
Browse files Browse the repository at this point in the history
…nterfaces (#831)

* Standardize to handle durations as Temporal.Duration in every inner interfaces

* Fix to choose schema parser

* Clarify min of wait-seconds-before-first-polling is increased to 1 from 0

* Apply same format again

* Use same format also in logging group

* Move readableDuration for report layer

* Improve formatter for long job

* Clarify not relative time
  • Loading branch information
kachick authored Jun 3, 2024
1 parent 15ec51f commit 3872903
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 120 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/GH-820-graceperiod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
- uses: ./
with:
retry-method: 'equal_intervals'
wait-seconds-before-first-polling: '0'
wait-seconds-before-first-polling: '1'
min-interval-seconds: '5'
attempt-limits: '100'
wait-list: |
Expand All @@ -77,7 +77,7 @@ jobs:
- uses: ./
with:
retry-method: 'equal_intervals'
wait-seconds-before-first-polling: '0'
wait-seconds-before-first-polling: '1'
min-interval-seconds: '5'
attempt-limits: '100'
wait-list: |
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This file only records notable changes. Not synchronized with all releases and t

- main - not yet released
- Add `startupGracePeriod` option in wait-list: [#820](https://github.com/kachick/wait-other-jobs/issues/820)
- Restrict `wait-seconds-before-first-polling` if it is too short as zero or shorter than `startupGracePeriod`
- v3.2.0
- Add `eventName` option in wait-list: [#771](https://github.com/kachick/wait-other-jobs/issues/771)
- v3.1.0
Expand Down
78 changes: 44 additions & 34 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31042,6 +31042,12 @@ var MyDurationLike = z2.object({
nanoseconds: z2.number().optional()
}).strict().readonly();
var Durationable = z2.union([z2.string().duration(), MyDurationLike]).transform((item) => getDuration(item));
var Duration = z2.instanceof(mr.Duration).refine(
(d2) => mr.Duration.compare(d2, { seconds: 0 }) > 0,
{
message: "Too short interval for pollings"
}
);
var defaultGrace = mr.Duration.from({ seconds: 10 });
function isDurationLike(my) {
for (const [_2, value] of Object.entries(my)) {
Expand All @@ -31053,7 +31059,7 @@ function isDurationLike(my) {
}
function getDuration(durationable) {
if (typeof durationable === "string" || isDurationLike(durationable)) {
return mr.Duration.from(durationable);
return Duration.parse(mr.Duration.from(durationable));
}
throw new Error("unexpected value is specified in durations");
}
Expand All @@ -31079,26 +31085,26 @@ var retryMethods = z2.enum(["exponential_backoff", "equal_intervals"]);
var Options = z2.object({
waitList: WaitList,
skipList: SkipList,
waitSecondsBeforeFirstPolling: z2.number().min(0),
minIntervalSeconds: z2.number().min(1),
initialDuration: Duration,
leastInterval: Duration,
retryMethod: retryMethods,
attemptLimits: z2.number().min(1),
isEarlyExit: z2.boolean(),
shouldSkipSameWorkflow: z2.boolean(),
isDryRun: z2.boolean()
}).readonly().refine(
}).strict().readonly().refine(
({ waitList, skipList }) => !(waitList.length > 0 && skipList.length > 0),
{ message: "Do not specify both wait-list and skip-list", path: ["waitList", "skipList"] }
).refine(
({ waitSecondsBeforeFirstPolling, waitList }) => waitList.every(
({ initialDuration, waitList }) => waitList.every(
(item) => !(mr.Duration.compare(
{ seconds: waitSecondsBeforeFirstPolling },
initialDuration,
item.startupGracePeriod
) > 0 && mr.Duration.compare(item.startupGracePeriod, defaultGrace) !== 0)
),
{
message: "A shorter startupGracePeriod waiting for the first poll does not make sense",
path: ["waitSecondsBeforeFirstPolling", "waitList"]
path: ["initialDuration", "waitList"]
}
);

Expand Down Expand Up @@ -31142,8 +31148,8 @@ function parseInput() {
const shouldSkipSameWorkflow = (0, import_core.getBooleanInput)("skip-same-workflow", { required: true, trimWhitespace: true });
const isDryRun = (0, import_core.getBooleanInput)("dry-run", { required: true, trimWhitespace: true });
const options = Options.parse({
waitSecondsBeforeFirstPolling,
minIntervalSeconds,
initialDuration: Durationable.parse({ seconds: waitSecondsBeforeFirstPolling }),
leastInterval: Durationable.parse({ seconds: minIntervalSeconds }),
retryMethod,
attemptLimits,
waitList: JSON.parse((0, import_core.getInput)("wait-list", { required: true })),
Expand Down Expand Up @@ -32360,6 +32366,17 @@ function groupBy(items, callback) {
}

// src/report.ts
function readableDuration(duration) {
const { hours, minutes, seconds } = duration.round({ largestUnit: "hours" });
const eachUnit = [`${seconds} seconds`];
if (minutes > 0) {
eachUnit.unshift(`${minutes} minutes`);
}
if (hours > 0) {
eachUnit.unshift(`${hours} hours`);
}
return `about ${eachUnit.join(" ")}`;
}
function summarize(check, trigger) {
const { checkRun: run2, checkSuite: suite, workflow, workflowRun } = check;
const isCompleted = run2.status === "COMPLETED";
Expand Down Expand Up @@ -32482,40 +32499,34 @@ function generateReport(summaries, trigger, elapsed, { waitList, skipList, shoul

// src/wait.ts
import { setTimeout as setTimeout2 } from "timers/promises";
var wait = setTimeout2;
var waitPrimitive = setTimeout2;
function wait(interval) {
return waitPrimitive(interval.total("milliseconds"));
}
function getRandomInt(min, max) {
const flooredMin = Math.ceil(min);
return Math.floor(Math.random() * (Math.floor(max) - flooredMin) + flooredMin);
}
function readableDuration(milliseconds) {
const msecToSec = 1e3;
const secToMin = 60;
const seconds = milliseconds / msecToSec;
const minutes = seconds / secToMin;
const { unit, value, precision } = minutes >= 1 ? { unit: "minutes", value: minutes, precision: 1 } : { unit: "seconds", value: seconds, precision: 0 };
const adjustor = 10 ** precision;
return `about ${(Math.round(value * adjustor) / adjustor).toFixed(
precision
)} ${unit}`;
}
var MIN_JITTER_MILLISECONDS = 1e3;
var MAX_JITTER_MILLISECONDS = 7e3;
function calcExponentialBackoffAndJitter(minIntervalSeconds, attempts) {
function calcExponentialBackoffAndJitter(leastInterval, attempts) {
const jitterMilliseconds = getRandomInt(MIN_JITTER_MILLISECONDS, MAX_JITTER_MILLISECONDS);
return minIntervalSeconds * 2 ** (attempts - 1) * 1e3 + jitterMilliseconds;
return mr.Duration.from({
milliseconds: leastInterval.total("milliseconds") * 2 ** (attempts - 1) + jitterMilliseconds
});
}
function getIdleMilliseconds(method, minIntervalSeconds, attempts) {
function getInterval(method, leastInterval, attempts) {
switch (method) {
case "exponential_backoff":
return calcExponentialBackoffAndJitter(
minIntervalSeconds,
leastInterval,
attempts
);
case "equal_intervals":
return minIntervalSeconds * 1e3;
return leastInterval;
default: {
const _exhaustiveCheck = method;
return minIntervalSeconds * 1e3;
return leastInterval;
}
}
}
Expand Down Expand Up @@ -32568,16 +32579,15 @@ async function run() {
break;
}
if (attempts === 1) {
const initialMsec = options.waitSecondsBeforeFirstPolling * 1e3;
(0, import_core3.info)(`Wait ${readableDuration(initialMsec)} before first polling.`);
await wait(initialMsec);
(0, import_core3.info)(`Wait ${readableDuration(options.initialDuration)} before first polling.`);
await wait(options.initialDuration);
} else {
const msec = getIdleMilliseconds(options.retryMethod, options.minIntervalSeconds, attempts);
(0, import_core3.info)(`Wait ${readableDuration(msec)} before next polling to reduce API calls.`);
await wait(msec);
const interval = getInterval(options.retryMethod, options.leastInterval, attempts);
(0, import_core3.info)(`Wait ${readableDuration(interval)} before next polling to reduce API calls.`);
await wait(interval);
}
const elapsed = mr.Duration.from({ milliseconds: Math.ceil(performance.now() - startedAt) });
(0, import_core3.startGroup)(`Polling ${attempts}: ${(/* @__PURE__ */ new Date()).toISOString()}(${elapsed.toString()}) ~`);
(0, import_core3.startGroup)(`Polling ${attempts}: ${(/* @__PURE__ */ new Date()).toISOString()} # total elapsed ${readableDuration(elapsed)})`);
const checks = await fetchChecks(githubToken, trigger);
if ((0, import_core3.isDebug)()) {
(0, import_core3.debug)(JSON.stringify({ label: "rawdata", checks, elapsed }, null, 2));
Expand Down
6 changes: 3 additions & 3 deletions src/input.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { debug, getInput, getBooleanInput, setSecret, isDebug, error } from '@actions/core';
import { context } from '@actions/github';

import { Options, Trigger } from './schema.ts';
import { Durationable, Options, Trigger } from './schema.ts';

export function parseInput(): { trigger: Trigger; options: Options; githubToken: string } {
const {
Expand Down Expand Up @@ -45,8 +45,8 @@ export function parseInput(): { trigger: Trigger; options: Options; githubToken:
const isDryRun = getBooleanInput('dry-run', { required: true, trimWhitespace: true });

const options = Options.parse({
waitSecondsBeforeFirstPolling,
minIntervalSeconds,
initialDuration: Durationable.parse({ seconds: waitSecondsBeforeFirstPolling }),
leastInterval: Durationable.parse({ seconds: minIntervalSeconds }),
retryMethod,
attemptLimits,
waitList: JSON.parse(getInput('wait-list', { required: true })),
Expand Down
17 changes: 8 additions & 9 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ function colorize(severity: Severity, message: string): string {

import { parseInput } from './input.ts';
import { fetchChecks } from './github-api.ts';
import { Severity, generateReport, getSummaries } from './report.ts';
import { readableDuration, wait, getIdleMilliseconds } from './wait.ts';
import { Severity, generateReport, getSummaries, readableDuration } from './report.ts';
import { getInterval, wait } from './wait.ts';
import { Temporal } from 'temporal-polyfill';

async function run(): Promise<void> {
Expand Down Expand Up @@ -58,18 +58,17 @@ async function run(): Promise<void> {
}

if (attempts === 1) {
const initialMsec = options.waitSecondsBeforeFirstPolling * 1000;
info(`Wait ${readableDuration(initialMsec)} before first polling.`);
await wait(initialMsec);
info(`Wait ${readableDuration(options.initialDuration)} before first polling.`);
await wait(options.initialDuration);
} else {
const msec = getIdleMilliseconds(options.retryMethod, options.minIntervalSeconds, attempts);
info(`Wait ${readableDuration(msec)} before next polling to reduce API calls.`);
await wait(msec);
const interval = getInterval(options.retryMethod, options.leastInterval, attempts);
info(`Wait ${readableDuration(interval)} before next polling to reduce API calls.`);
await wait(interval);
}

// Put getting elapsed time before of fetchChecks to keep accuracy of the purpose
const elapsed = Temporal.Duration.from({ milliseconds: Math.ceil(performance.now() - startedAt) });
startGroup(`Polling ${attempts}: ${(new Date()).toISOString()}(${elapsed.toString()}) ~`);
startGroup(`Polling ${attempts}: ${(new Date()).toISOString()} # total elapsed ${readableDuration(elapsed)})`);
const checks = await fetchChecks(githubToken, trigger);
if (isDebug()) {
debug(JSON.stringify({ label: 'rawdata', checks, elapsed }, null, 2));
Expand Down
11 changes: 10 additions & 1 deletion src/report.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import test from 'node:test';
import assert from 'node:assert';
import { checks8679817057, checks92810686811WaitSuccessPolling1 } from './snapshot.ts';
import { Report, Summary, generateReport, getSummaries } from './report.ts';
import { Report, Summary, generateReport, getSummaries, readableDuration } from './report.ts';
import { omit } from './util.ts';
import { Temporal } from 'temporal-polyfill';
import { jsonEqual } from './assert.ts';

test('readableDuration', () => {
assert.strictEqual(readableDuration(Temporal.Duration.from({ milliseconds: 454356 })), 'about 7 minutes 34 seconds');
assert.strictEqual(readableDuration(Temporal.Duration.from({ milliseconds: 32100 })), 'about 32 seconds');
assert.strictEqual(
readableDuration(Temporal.Duration.from({ hours: 4, minutes: 100, seconds: 79 })),
'about 5 hours 41 minutes 19 seconds',
);
});

const exampleSummary = Object.freeze(
{
isAcceptable: false,
Expand Down
12 changes: 12 additions & 0 deletions src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import { join, relative } from 'path';
import { Temporal } from 'temporal-polyfill';
import { groupBy } from './util.ts';

export function readableDuration(duration: Temporal.Duration): string {
const { hours, minutes, seconds } = duration.round({ largestUnit: 'hours' });
const eachUnit = [`${seconds} seconds`];
if (minutes > 0) {
eachUnit.unshift(`${minutes} minutes`);
}
if (hours > 0) {
eachUnit.unshift(`${hours} hours`);
}
return `about ${eachUnit.join(' ')}`;
}

export interface Summary {
isAcceptable: boolean;
isCompleted: boolean;
Expand Down
26 changes: 13 additions & 13 deletions src/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from 'node:test';
import { deepStrictEqual, throws } from 'node:assert';
import { throws } from 'node:assert';
import { Durationable, Options } from './schema.ts';
import { Temporal } from 'temporal-polyfill';
import { durationEqual, optionsEqual } from './assert.ts';
Expand All @@ -9,21 +9,21 @@ const defaultOptions = Object.freeze({
attemptLimits: 1000,
waitList: [],
skipList: [],
waitSecondsBeforeFirstPolling: 10,
minIntervalSeconds: 15,
initialDuration: Temporal.Duration.from({ seconds: 10 }),
leastInterval: Temporal.Duration.from({ seconds: 15 }),
retryMethod: 'equal_intervals',
shouldSkipSameWorkflow: false,
isDryRun: false,
});

test('Options keep given values', () => {
deepStrictEqual({
optionsEqual({
isEarlyExit: true,
attemptLimits: 1000,
waitList: [],
skipList: [],
waitSecondsBeforeFirstPolling: 10,
minIntervalSeconds: 15,
initialDuration: Temporal.Duration.from({ seconds: 10 }),
leastInterval: Temporal.Duration.from({ seconds: 15 }),
retryMethod: 'equal_intervals',
shouldSkipSameWorkflow: false,
isDryRun: false,
Expand All @@ -45,9 +45,9 @@ test('Options set some default values it cannot be defined in action.yml', () =>
});

test('Options reject invalid values', () => {
throws(() => Options.parse({ ...defaultOptions, minIntervalSeconds: 0 }), {
throws(() => Options.parse({ ...defaultOptions, leastInterval: Temporal.Duration.from({ seconds: 0 }) }), {
name: 'ZodError',
message: /too_small/,
message: /Too short interval for pollings/,
});

throws(() => Options.parse({ ...defaultOptions, attemptLimits: 0 }), {
Expand Down Expand Up @@ -182,7 +182,7 @@ test('wait-list have startupGracePeriod', async (t) => {
() =>
Options.parse({
...defaultOptions,
waitSecondsBeforeFirstPolling: 41,
initialDuration: Temporal.Duration.from({ seconds: 41 }),
waitList: [{ workflowFile: 'ci.yml', startupGracePeriod: { seconds: 40 } }],
}),
{
Expand All @@ -196,12 +196,12 @@ test('wait-list have startupGracePeriod', async (t) => {
optionsEqual(
Options.parse({
...defaultOptions,
waitSecondsBeforeFirstPolling: 42,
initialDuration: Temporal.Duration.from({ seconds: 42 }),
waitList: [{ workflowFile: 'ci.yml', startupGracePeriod: { seconds: 10 } }],
}),
{
...defaultOptions,
waitSecondsBeforeFirstPolling: 42,
initialDuration: Temporal.Duration.from({ seconds: 42 }),
waitList: [{
workflowFile: 'ci.yml',
optional: false,
Expand All @@ -213,12 +213,12 @@ test('wait-list have startupGracePeriod', async (t) => {
optionsEqual(
Options.parse({
...defaultOptions,
waitSecondsBeforeFirstPolling: 42,
initialDuration: Temporal.Duration.from({ seconds: 42 }),
waitList: [{ workflowFile: 'ci.yml' }],
}),
{
...defaultOptions,
waitSecondsBeforeFirstPolling: 42,
initialDuration: Temporal.Duration.from({ seconds: 42 }),
waitList: [{
workflowFile: 'ci.yml',
optional: false,
Expand Down
Loading

0 comments on commit 3872903

Please sign in to comment.