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
  • Loading branch information
kachick committed Jun 3, 2024
1 parent 15ec51f commit 675872e
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 107 deletions.
71 changes: 39 additions & 32 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: Duration.parse({ seconds: waitSecondsBeforeFirstPolling }),
leastInterval: Duration.parse({ seconds: minIntervalSeconds }),
retryMethod,
attemptLimits,
waitList: JSON.parse((0, import_core.getInput)("wait-list", { required: true })),
Expand Down Expand Up @@ -32482,40 +32488,42 @@ 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}`;
function readableDuration(duration) {
const { minutes, seconds } = duration.round({ largestUnit: "minutes" });
const eachUnit = [`${seconds} seconds`];
if (minutes > 0) {
eachUnit.unshift(`${minutes} minutes`);
}
return `about ${eachUnit.join(" ")}`;
}
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,13 +32576,12 @@ 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 ${options.initialDuration.toString()} 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()}) ~`);
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 { Duration, 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: Duration.parse({ seconds: waitSecondsBeforeFirstPolling }),
leastInterval: Duration.parse({ seconds: minIntervalSeconds }),
retryMethod,
attemptLimits,
waitList: JSON.parse(getInput('wait-list', { required: true })),
Expand Down
13 changes: 6 additions & 7 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ 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 { readableDuration, getInterval, wait } from './wait.ts';
import { Temporal } from 'temporal-polyfill';

async function run(): Promise<void> {
Expand Down Expand Up @@ -58,13 +58,12 @@ 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 ${options.initialDuration.toString()} 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
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
23 changes: 15 additions & 8 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ type MyDurationLike = z.infer<typeof MyDurationLike>;
// IETF does not define duration formats in their RFCs, but in RFC 3399 refers ISO 8601 duration formats.
// https://www.ietf.org/rfc/rfc3339.txt
export const Durationable = z.union([z.string().duration(), MyDurationLike]).transform((item) => getDuration(item));
export const Duration = z.instanceof(Temporal.Duration).refine(
(d) => Temporal.Duration.compare(d, { seconds: 0 }) > 0,
{
message: 'Too short interval for pollings',
},
);
type Duration = z.infer<typeof Duration>;
const defaultGrace = Temporal.Duration.from({ seconds: 10 });

// workaround for https://github.com/colinhacks/zod/issues/635
Expand All @@ -51,9 +58,9 @@ function isDurationLike(my: MyDurationLike): my is DurationLike {
}

// workaround for https://github.com/colinhacks/zod/issues/635
export function getDuration(durationable: string | MyDurationLike): Temporal.Duration {
export function getDuration(durationable: string | MyDurationLike): Duration {
if (typeof durationable === 'string' || isDurationLike(durationable)) {
return Temporal.Duration.from(durationable);
return Duration.parse(Temporal.Duration.from(durationable));
}

throw new Error('unexpected value is specified in durations');
Expand Down Expand Up @@ -89,29 +96,29 @@ export type RetryMethod = z.infer<typeof retryMethods>;
export const Options = z.object({
waitList: WaitList,
skipList: SkipList,
waitSecondsBeforeFirstPolling: z.number().min(0),
minIntervalSeconds: z.number().min(1),
initialDuration: Duration,
leastInterval: Duration,
retryMethod: retryMethods,
attemptLimits: z.number().min(1),
isEarlyExit: z.boolean(),
shouldSkipSameWorkflow: z.boolean(),
isDryRun: z.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 }) =>
({ initialDuration, waitList }) =>
waitList.every(
(item) =>
!(Temporal.Duration.compare(
{ seconds: waitSecondsBeforeFirstPolling },
initialDuration,
item.startupGracePeriod,
) > 0
&& Temporal.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
Loading

0 comments on commit 675872e

Please sign in to comment.