Wait for, acquire, and release a distributed lock via S3 storage.
- uses: aws-actions/configure-aws-credentials@v4
with:
# ...
# This blocks until the lock is acquired, or errors if timeout is reached
- uses: freckle/aws-s3-lock-action@v1
with:
# Required
bucket: an-existing-s3-bucket
# Optional, defaults shown
# name: {workflow}/{job}
# expires: 15m
# timeout: {matches expires}
# timeout-poll: 5s
# context: "{workflow} #{run}"
- run: echo "Lock held, do work here"
The lock is released (the S3 object deleted) in our Post step, which provides a pretty robust guarantee of release. Expired locks are ignored (not deleted), so it's recommended you put a Lifecyle policy on the Bucket to clean them up after some time.
jobs:
acquire-lock:
# ...
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
# ...
# This blocks until the lock is acquired, or errors if timeout is reached
- id: lock
uses: freckle/aws-s3-lock-action/acquire@v1
with:
# Required
bucket: an-existing-s3-bucket
# Optional, defaults shown
# name: {workflow}/{job}
# expires: 15m
# timeout: {matches expires}
# timeout-poll: 5s
# context: "{workflow} #{run}"
outputs:
key: ${{ steps.lock.outputs.key }}
work:
# ...
needs:
- acquire-lock
steps:
- run: echo "Lock held, do work here"
release:
# ...
if: ${{ always() }}
needs:
- acquire-lock
- work
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
# ...
- uses: freckle/aws-s3-lock-action/release@v1
with:
bucket: an-existing-s3-bucket
key: ${{ needs.acquire-lock.outputs.key }}
name | description | required | default |
---|---|---|---|
bucket |
Name of an existing S3 bucket to use. |
true |
"" |
name |
Name for the lock object. Include any prefix you want within the bucket. The key will be built as "name.created.uuid.expires". |
false |
${{ github.workflow }}/${{ github.job }} |
expires |
How long before concurrent operations consider this lock expired. |
false |
15m |
timeout |
How long to wait for the lock to become available. Default matches expires. |
false |
"" |
timeout-poll |
How long to wait between attempts for the lock. Default is 5s. |
false |
5s |
context |
Additional context to write as the body of the lock file. Concurrent operations waiting on this lock will display it. |
false |
${{ github.workflow }} #${{ github.run_number }} |
name | description |
---|---|
key |
Key of the S3 object representing the lock |
acquired-at |
Timestamp the lock was acquired |
This tool implements a version of the locking algorithm described in this StackOverflow answer.
-
Upload a lock object to S3 at
<name>.<created>.<uuid>.<expires>
All time values are milliseconds since epoch.
-
List all other lock objects (prefix
<name>.
)Filter out any expired keys (looking at
expires
) and sort, which implicitly means bycreated
thenuuid
as desired. -
If the first one (i.e. oldest) is our own, we've acquired the lock
-
If not, we lost the race; remove our object, wait, and try again
Each time we attempt to acquire the lock, we create a new key name (e.g.
<created>
and <expires>
both change), this effectively loses our "place in
line" but ensures that expiry is measured from time of acquisition and not time
of first attempt. There are trade-offs either way, and possible room for
improvement, so this is just how we're doing it for now.
This tool is not meant to be bullet-proof. We built it for our needs and accept that there are simply no strong guarantees in this locking mechanism's operation at scale. Your mileage may vary; patches welcome.
Versioned tags will exist, such as v1.0.0
and v2.1.1
. Branches will exist
for each major version, such as v1
or v2
and contain the newest version in
that series.
Given a latest version of v1.0.1,
Is this a new major version?
If yes,
git checkout main
git pull
git checkout -b v2
git tag -s -m v2.0.0 v2.0.0
git push --follow-tags
Otherwise,
git checkout main
git pull
git checkout v1
git merge --ff-only -
git tag -s -m v1.0.2 v1.0.2 # or v1.1.0
git push --follow-tags