Skip to content

Commit

Permalink
Implement context input
Browse files Browse the repository at this point in the history
If given and un-empty, the `context` string is written as the body of
the lock object on S3. When a lock is acquired or found blocking, that
context is read and included in the details show.

This can be used to supply a sort of "about me" value at acquire time,
so that we can display a sort of "held by" when waiting.

By default, this input is the workflow and run number, which means
waiting on a lock in one run of a workflow will now show you which other
run of that workflow is currently holding it.
  • Loading branch information
pbrisbin committed Nov 21, 2023
1 parent 60470e5 commit 6b58fac
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 10 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Wait for, acquire, and release a distributed lock via S3 storage.
# expires: 15m
# timeout: {matches expires}
# timeout-poll: 5s
# context: "{workflow} #{run}"

- run: echo "Lock held, do work here"
```
Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ inputs:
How long to wait between attempts for the lock. Default is 5s.
required: true
default: 5s
context:
description:
Additional context to write as the body of the lock file. Concurrent
operations waiting on this lock will display it.
required: true
default: "${{ github.workflow }} #${{ github.run_number }}"
outputs:
key:
description: "Key of the S3 object representing the lock"
Expand Down
30 changes: 25 additions & 5 deletions src/S3Lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
S3Client,
ListObjectsV2Command,
DeleteObjectCommand,
GetObjectCommand,
} from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";

Expand All @@ -29,13 +30,13 @@ export class S3Lock {
this.s3 = new S3Client();
}

async acquireLock(): Promise<AcquireLockResult> {
async acquireLock(body: string): Promise<AcquireLockResult> {
const key = createObjectKey(this.prefix, this.expires);

core.debug(`[s3] Upload ${key}`);
const upload = new Upload({
client: this.s3,
params: { Bucket: this.bucket, Key: key, Body: "" },
params: { Bucket: this.bucket, Key: key, Body: body },
});
await upload.done();

Expand Down Expand Up @@ -70,17 +71,36 @@ export class S3Lock {
return { tag: "not-acquired", blockingKey: keys[0] };
}

objectKeyDetails(key: string): string {
async objectKeyDetails(key: string): Promise<string> {
const end = key.slice(this.prefix.length);
const { uuid, createdAt, expiresAt } = S3LockExt.fromString(end);
const created = Duration.since(createdAt);
const expires = Duration.until(expiresAt);

return [
let context;

try {
const obj = await this.s3.send(
new GetObjectCommand({
Bucket: this.bucket,
Key: key,
}),
);
context = await obj.Body?.transformToString();
} catch (ex) {
core.warning(`Unable to read object body ${ex}`);
}

const contextLines =
context && context === "" ? [] : [`Context: ${color.gray(context)}`];

const messageLines = [
`${uuid}`,
`Created: ${color.gray(createdAt)} (${color.cyan(created)} ago)`,
`Expires: ${color.gray(expiresAt)} (${color.cyan(expires)} from now)`,
].join("\n ");
].concat(contextLines);

return messageLines.join("\n ");
}

static async releaseLock(bucket: string, key: string): Promise<void> {
Expand Down
9 changes: 5 additions & 4 deletions src/acquire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ import * as color from "./color";

async function run() {
try {
const { name, bucket, expires, timeout, timeoutPoll } = getInputs();
const { name, bucket, expires, timeout, timeoutPoll, context } =
getInputs();

const timer = new Timer(timeout);
const s3Lock = new S3Lock(bucket, name, expires);

while (true) {
let result = await s3Lock.acquireLock();
let result = await s3Lock.acquireLock(context);

if (result.tag === "acquired") {
const key = result.acquiredKey;
const keyDetails = s3Lock.objectKeyDetails(key);
const keyDetails = await s3Lock.objectKeyDetails(key);
core.info(
`Lock ${color.bold(name)} ${color.green(
"acquired",
Expand All @@ -30,7 +31,7 @@ async function run() {
}

const key = result.blockingKey;
const keyDetails = s3Lock.objectKeyDetails(key);
const keyDetails = await s3Lock.objectKeyDetails(key);

if (timer.expired()) {
core.error(
Expand Down
4 changes: 3 additions & 1 deletion src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type Inputs = {
expires: Duration;
timeout: Duration;
timeoutPoll: Duration;
context: string;
};

export function getInputs(): Inputs {
Expand All @@ -19,10 +20,11 @@ export function getInputs(): Inputs {
const rawExpires = core.getInput("expires", { required: true });
const rawTimeout = core.getInput("timeout", { required: false });
const rawTimeoutPoll = core.getInput("timeout-poll", { required: true });
const context = core.getInput("context", { required: false });

const expires = Duration.parse(rawExpires);
const timeout = rawTimeout === "" ? expires : Duration.parse(rawTimeout);
const timeoutPoll = Duration.parse(rawTimeoutPoll);

return { name, bucket, expires, timeout, timeoutPoll };
return { name, bucket, expires, timeout, timeoutPoll, context };
}

0 comments on commit 6b58fac

Please sign in to comment.