diff --git a/lambdas/functions/control-plane/package.json b/lambdas/functions/control-plane/package.json index 8f978c0d17..0e8fd44894 100644 --- a/lambdas/functions/control-plane/package.json +++ b/lambdas/functions/control-plane/package.json @@ -24,7 +24,6 @@ "@vercel/ncc": "^0.38.4", "aws-sdk-client-mock": "^4.1.0", "aws-sdk-client-mock-jest": "^4.1.0", - "moment-timezone": "^0.6.0", "nock": "^14.0.10", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0" @@ -41,7 +40,8 @@ "@octokit/plugin-retry": "8.0.3", "@octokit/plugin-throttling": "11.0.3", "@octokit/rest": "22.0.1", - "cron-parser": "^5.4.0" + "cron-parser": "^5.4.0", + "temporal-polyfill": "^0.3.2" }, "nx": { "includedScripts": [ diff --git a/lambdas/functions/control-plane/src/aws/runners.ts b/lambdas/functions/control-plane/src/aws/runners.ts index 0240d86a77..f481271e2e 100644 --- a/lambdas/functions/control-plane/src/aws/runners.ts +++ b/lambdas/functions/control-plane/src/aws/runners.ts @@ -15,7 +15,7 @@ import { import { createChildLogger } from '@aws-github-runner/aws-powertools-util'; import { getTracedAWSV3Client, tracer } from '@aws-github-runner/aws-powertools-util'; import { getParameter } from '@aws-github-runner/aws-ssm-util'; -import moment from 'moment'; +import { Temporal } from 'temporal-polyfill'; import ScaleError from './../scale-runners/ScaleError'; import * as Runners from './runners.d'; @@ -380,7 +380,12 @@ async function createInstancesWithRunInstances( // If launchTime is undefined, this will return false export function bootTimeExceeded(ec2Runner: { launchTime?: Date }): boolean { - const runnerBootTimeInMinutes = process.env.RUNNER_BOOT_TIME_IN_MINUTES; - const launchTimePlusBootTime = moment(ec2Runner.launchTime).utc().add(runnerBootTimeInMinutes, 'minutes'); - return launchTimePlusBootTime < moment(new Date()).utc(); + const runnerBootTimeInMinutes = Number(process.env.RUNNER_BOOT_TIME_IN_MINUTES); + if (ec2Runner.launchTime === undefined || !Number.isFinite(runnerBootTimeInMinutes)) { + return false; + } + const launchTimePlusBootTime = Temporal.Instant.fromEpochMilliseconds(ec2Runner.launchTime.getTime()).add({ + minutes: runnerBootTimeInMinutes, + }); + return Temporal.Instant.compare(launchTimePlusBootTime, Temporal.Now.instant()) < 0; } diff --git a/lambdas/functions/control-plane/src/pool/pool.test.ts b/lambdas/functions/control-plane/src/pool/pool.test.ts index ee4e36a463..e9185a38e2 100644 --- a/lambdas/functions/control-plane/src/pool/pool.test.ts +++ b/lambdas/functions/control-plane/src/pool/pool.test.ts @@ -1,5 +1,5 @@ import { Octokit } from '@octokit/rest'; -import moment from 'moment-timezone'; +import { Temporal } from 'temporal-polyfill'; import * as nock from 'nock'; import { listEC2Runners } from '../aws/runners'; @@ -57,6 +57,10 @@ const cleanEnv = process.env; const ORG = 'my-org'; const MINIMUM_TIME_RUNNING = 15; +function minutesAgo(minutes: number): Date { + return new Date(Temporal.Now.instant().subtract({ minutes }).epochMilliseconds); +} + const ec2InstancesRegistered = [ { instanceId: 'i-1-idle', @@ -78,9 +82,7 @@ const ec2InstancesRegistered = [ }, { instanceId: 'i-4-idle-older-than-minimum-time-running', - launchTime: moment(new Date()) - .subtract(MINIMUM_TIME_RUNNING + 3, 'minutes') - .toDate(), + launchTime: minutesAgo(MINIMUM_TIME_RUNNING + 3), type: 'Org', owner: ORG, }, @@ -211,17 +213,13 @@ describe('Test simple pool.', () => { ...ec2InstancesRegistered, { instanceId: 'i-4-still-booting', - launchTime: moment(new Date()) - .subtract(MINIMUM_TIME_RUNNING - 3, 'minutes') - .toDate(), + launchTime: minutesAgo(MINIMUM_TIME_RUNNING - 3), type: 'Org', owner: ORG, }, { instanceId: 'i-5-orphan', - launchTime: moment(new Date()) - .subtract(MINIMUM_TIME_RUNNING + 3, 'minutes') - .toDate(), + launchTime: minutesAgo(MINIMUM_TIME_RUNNING + 3), type: 'Org', owner: ORG, }, @@ -242,17 +240,13 @@ describe('Test simple pool.', () => { ...ec2InstancesRegistered, { instanceId: 'i-4-still-booting', - launchTime: moment(new Date()) - .subtract(MINIMUM_TIME_RUNNING - 3, 'minutes') - .toDate(), + launchTime: minutesAgo(MINIMUM_TIME_RUNNING - 3), type: 'Org', owner: ORG, }, { instanceId: 'i-5-orphan', - launchTime: moment(new Date()) - .subtract(MINIMUM_TIME_RUNNING + 3, 'minutes') - .toDate(), + launchTime: minutesAgo(MINIMUM_TIME_RUNNING + 3), type: 'Org', owner: ORG, }, diff --git a/lambdas/functions/control-plane/src/scale-runners/scale-down-config.test.ts b/lambdas/functions/control-plane/src/scale-runners/scale-down-config.test.ts index ff2325128a..b5a4ad36f7 100644 --- a/lambdas/functions/control-plane/src/scale-runners/scale-down-config.test.ts +++ b/lambdas/functions/control-plane/src/scale-runners/scale-down-config.test.ts @@ -1,4 +1,4 @@ -import moment from 'moment-timezone'; +import { Temporal } from 'temporal-polyfill'; import { EvictionStrategy, ScalingDownConfigList, getEvictionStrategy, getIdleRunnerCount } from './scale-down-config'; import { describe, it, expect } from 'vitest'; @@ -6,7 +6,8 @@ import { describe, it, expect } from 'vitest'; const DEFAULT_TIMEZONE = 'America/Los_Angeles'; const DEFAULT_IDLE_COUNT = 1; const DEFAULT_EVICTION_STRATEGY: EvictionStrategy = 'oldest_first'; -const now = moment.tz(new Date(), 'America/Los_Angeles'); +const now = Temporal.Now.zonedDateTimeISO('America/Los_Angeles'); +const nowDay = now.dayOfWeek % 7; function getConfig( cronTabs: string[], @@ -28,12 +29,12 @@ describe('scaleDownConfig', () => { }); it('No active cron configuration', async () => { - const scaleDownConfig = getConfig(['* * * * * ' + ((now.day() + 1) % 7)]); + const scaleDownConfig = getConfig(['* * * * * ' + ((nowDay + 1) % 7)]); expect(getIdleRunnerCount(scaleDownConfig)).toEqual(0); }); it('1 of 2 cron configurations be active', async () => { - const scaleDownConfig = getConfig(['* * * * * ' + ((now.day() + 1) % 7), '* * * * * ' + (now.day() % 7)]); + const scaleDownConfig = getConfig(['* * * * * ' + ((nowDay + 1) % 7), '* * * * * ' + (nowDay % 7)]); expect(getIdleRunnerCount(scaleDownConfig)).toEqual(DEFAULT_IDLE_COUNT); }); }); @@ -50,7 +51,7 @@ describe('scaleDownConfig', () => { }); it('No active cron configuration', async () => { - const scaleDownConfig = getConfig(['* * * * * ' + ((now.day() + 1) % 7)]); + const scaleDownConfig = getConfig(['* * * * * ' + ((nowDay + 1) % 7)]); expect(getEvictionStrategy(scaleDownConfig)).toEqual(DEFAULT_EVICTION_STRATEGY); }); }); diff --git a/lambdas/functions/control-plane/src/scale-runners/scale-down-config.ts b/lambdas/functions/control-plane/src/scale-runners/scale-down-config.ts index 2d28c38cfc..3010441a70 100644 --- a/lambdas/functions/control-plane/src/scale-runners/scale-down-config.ts +++ b/lambdas/functions/control-plane/src/scale-runners/scale-down-config.ts @@ -1,6 +1,6 @@ import { createChildLogger } from '@aws-github-runner/aws-powertools-util'; import parser from 'cron-parser'; -import moment from 'moment'; +import { Temporal } from 'temporal-polyfill'; export type ScalingDownConfigList = ScalingDownConfig[]; export type EvictionStrategy = 'newest_first' | 'oldest_first'; @@ -14,12 +14,13 @@ export interface ScalingDownConfig { const logger = createChildLogger('scale-down-config.ts'); function inPeriod(period: ScalingDownConfig): boolean { - const now = moment(new Date()); + const now = Temporal.Now.instant(); const expr = parser.parse(period.cron, { tz: period.timeZone, }); - const next = moment(expr.next().toDate()); - return Math.abs(next.diff(now, 'seconds')) < 5; // we keep a range of 5 seconds + const next = Temporal.Instant.fromEpochMilliseconds(expr.next().toDate().getTime()); + const diffInSeconds = Math.trunc(now.until(next).total('seconds')); + return Math.abs(diffInSeconds) < 5; // we keep a range of 5 seconds } export function getIdleRunnerCount(scalingDownConfigs: ScalingDownConfigList): number { diff --git a/lambdas/functions/control-plane/src/scale-runners/scale-down.test.ts b/lambdas/functions/control-plane/src/scale-runners/scale-down.test.ts index 2dfb190a38..0ee8315a8d 100644 --- a/lambdas/functions/control-plane/src/scale-runners/scale-down.test.ts +++ b/lambdas/functions/control-plane/src/scale-runners/scale-down.test.ts @@ -1,6 +1,6 @@ import { Octokit } from '@octokit/rest'; import { RequestError } from '@octokit/request-error'; -import moment from 'moment'; +import { Temporal } from 'temporal-polyfill'; import nock from 'nock'; import { RunnerInfo, RunnerList } from '../aws/runners.d'; @@ -76,6 +76,10 @@ export interface TestData { const cleanEnv = process.env; +function minutesAgo(minutes: number): Date { + return new Date(Temporal.Now.instant().subtract({ minutes }).epochMilliseconds); +} + const ENVIRONMENT = 'unit-test-environment'; const MINIMUM_TIME_RUNNING_IN_MINUTES = 30; const MINIMUM_BOOT_TIME = 5; @@ -680,25 +684,25 @@ describe('Scale down runners', () => { const runners: RunnerInfo[] = [ { instanceId: '1', - launchTime: moment(new Date()).subtract(1, 'minute').toDate(), + launchTime: minutesAgo(1), owner: 'owner', type: 'type', }, { instanceId: '3', - launchTime: moment(new Date()).subtract(3, 'minute').toDate(), + launchTime: minutesAgo(3), owner: 'owner', type: 'type', }, { instanceId: '2', - launchTime: moment(new Date()).subtract(2, 'minute').toDate(), + launchTime: minutesAgo(2), owner: 'owner', type: 'type', }, { instanceId: '0', - launchTime: moment(new Date()).subtract(0, 'minute').toDate(), + launchTime: minutesAgo(0), owner: 'owner', type: 'type', }, @@ -722,7 +726,7 @@ describe('Scale down runners', () => { it('Should sort runners with equal launch time.', () => { const runnersTest = [...runners]; - const same = moment(new Date()).subtract(4, 'minute').toDate(); + const same = minutesAgo(4); runnersTest.push({ instanceId: '4', launchTime: same, @@ -756,7 +760,7 @@ describe('Scale down runners', () => { }, { instanceId: '1', - launchTime: moment(new Date()).subtract(3, 'minute').toDate(), + launchTime: minutesAgo(3), owner: 'owner', type: 'type', }, @@ -821,7 +825,7 @@ function createRunnerTestData( ): RunnerTestItem { return { instanceId: `i-${name}-${type.toLowerCase()}`, - launchTime: moment(new Date()).subtract(minutesLaunchedAgo, 'minutes').toDate(), + launchTime: minutesAgo(minutesLaunchedAgo), type, owner: owner ? owner diff --git a/lambdas/functions/control-plane/src/scale-runners/scale-down.ts b/lambdas/functions/control-plane/src/scale-runners/scale-down.ts index 6086af7714..40bc38c645 100644 --- a/lambdas/functions/control-plane/src/scale-runners/scale-down.ts +++ b/lambdas/functions/control-plane/src/scale-runners/scale-down.ts @@ -2,7 +2,7 @@ import { Octokit } from '@octokit/rest'; import { Endpoints } from '@octokit/types'; import { RequestError } from '@octokit/request-error'; import { createChildLogger } from '@aws-github-runner/aws-powertools-util'; -import moment from 'moment'; +import { Temporal } from 'temporal-polyfill'; import { createGithubAppAuth, createGithubInstallationAuth, createOctokitClient } from '../github/auth'; import { bootTimeExceeded, listEC2Runners, tag, untag, terminateRunner } from './../aws/runners'; @@ -121,10 +121,14 @@ async function listGitHubRunners(runner: RunnerInfo): Promise { } function runnerMinimumTimeExceeded(runner: RunnerInfo): boolean { - const minimumRunningTimeInMinutes = process.env.MINIMUM_RUNNING_TIME_IN_MINUTES; - const launchTimePlusMinimum = moment(runner.launchTime).utc().add(minimumRunningTimeInMinutes, 'minutes'); - const now = moment(new Date()).utc(); - return launchTimePlusMinimum < now; + const minimumRunningTimeInMinutes = Number(process.env.MINIMUM_RUNNING_TIME_IN_MINUTES); + if (runner.launchTime === undefined || !Number.isFinite(minimumRunningTimeInMinutes)) { + return false; + } + const launchTimePlusMinimum = Temporal.Instant.fromEpochMilliseconds(runner.launchTime.getTime()).add({ + minutes: minimumRunningTimeInMinutes, + }); + return Temporal.Instant.compare(launchTimePlusMinimum, Temporal.Now.instant()) < 0; } async function removeRunner(ec2runner: RunnerInfo, ghRunnerIds: number[]): Promise { diff --git a/lambdas/yarn.lock b/lambdas/yarn.lock index 2ca672d961..bd2f6a7809 100644 --- a/lambdas/yarn.lock +++ b/lambdas/yarn.lock @@ -165,8 +165,8 @@ __metadata: aws-sdk-client-mock: "npm:^4.1.0" aws-sdk-client-mock-jest: "npm:^4.1.0" cron-parser: "npm:^5.4.0" - moment-timezone: "npm:^0.6.0" nock: "npm:^14.0.10" + temporal-polyfill: "npm:^0.3.2" ts-node: "npm:^10.9.2" ts-node-dev: "npm:^2.0.0" languageName: unknown @@ -8943,22 +8943,6 @@ __metadata: languageName: node linkType: hard -"moment-timezone@npm:^0.6.0": - version: 0.6.0 - resolution: "moment-timezone@npm:0.6.0" - dependencies: - moment: "npm:^2.29.4" - checksum: 10c0/16164cf321d8be0bf7d43855286b426c94c8200e0634f2e42cf469f591c6a230ac43f37d3826d76b05ac221f69a571400323fb8625e3d4e8669f4d9ab00fe779 - languageName: node - linkType: hard - -"moment@npm:^2.29.4": - version: 2.29.4 - resolution: "moment@npm:2.29.4" - checksum: 10c0/844c6f3ce42862ac9467c8ca4f5e48a00750078682cc5bda1bc0e50cc7ca88e2115a0f932d65a06e4a90e26cb78892be9b3ca3dd6546ca2c4d994cebb787fc2b - languageName: node - linkType: hard - "ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -10651,6 +10635,22 @@ __metadata: languageName: node linkType: hard +"temporal-polyfill@npm:^0.3.2": + version: 0.3.2 + resolution: "temporal-polyfill@npm:0.3.2" + dependencies: + temporal-spec: "npm:0.3.1" + checksum: 10c0/762795c94e1b6cb0574541d0b09f805ff23b0a2c9d0356ab3fc0205e390a3bce73e047de2fdc54c7d149d6e6d781aa1d95aa62afa5de4c83cb79c129b0e502da + languageName: node + linkType: hard + +"temporal-spec@npm:0.3.1": + version: 0.3.1 + resolution: "temporal-spec@npm:0.3.1" + checksum: 10c0/e38eae391bd59ae675f1925a65c8ef302933cf9ce98efedaf2e69269e8c36f2b5d486cd3084367f04a6c1ea0289b925690c0df20a73f0c4629b531775924e850 + languageName: node + linkType: hard + "tinybench@npm:^2.9.0": version: 2.9.0 resolution: "tinybench@npm:2.9.0"