carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package lease
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "time"
10
11 "github.com/aws/aws-sdk-go-v2/aws"
12 awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
13 "github.com/aws/smithy-go"
14 "github.com/benbjohnson/litestream"
15 lss3 "github.com/benbjohnson/litestream/s3"
16)
17
18// errLockPresent is createLease's answer when a lock object exists, live or
19// expired: litestream's AcquireLease decides it, as it always has.
20var errLockPresent = errors.New("lease: a lock is present")
21
22// createLease is litestream's AcquireLease for the common case, the lock
23// absent (a release deletes it), in one round trip instead of two. It
24// writes what AcquireLease writes for an absent lock: generation 1, the
25// leaser's owner, now plus its TTL, the same JSON and content type, and the
26// same create-only condition (If-None-Match: *). So the lock it leaves, and
27// what a reader of it decides, are AcquireLease's. A 412 means a lock is
28// there, and errLockPresent hands the decision back to AcquireLease (the
29// takeover of an expired lock, the refusal of a live one). Any other error
30// is the acquire's, as AcquireLease's own write failing would be.
31func createLease(ctx context.Context, client *awss3.Client, l *lss3.Leaser) (*litestream.Lease, error) {
32 lease := &litestream.Lease{Generation: 1, ExpiresAt: time.Now().Add(l.TTL), Owner: l.Owner}
33 data, err := json.Marshal(lease)
34 if err != nil {
35 return nil, fmt.Errorf("marshal lock file: %w", err)
36 }
37 key := lss3.DefaultLeasePath
38 if l.Path != "" {
39 key = l.Path + "/" + lss3.DefaultLeasePath
40 }
41 out, err := client.PutObject(ctx, &awss3.PutObjectInput{
42 Bucket: aws.String(l.Bucket),
43 Key: aws.String(key),
44 Body: bytes.NewReader(data),
45 ContentType: aws.String("application/json"),
46 IfNoneMatch: aws.String("*"),
47 })
48 if err != nil {
49 if preconditionFailed(err) {
50 return nil, errLockPresent
51 }
52 return nil, fmt.Errorf("put lock file: %w", err)
53 }
54 if out.ETag != nil {
55 lease.ETag = *out.ETag
56 }
57 return lease, nil
58}
59
60// preconditionFailed is litestream's isPreconditionFailed (unexported
61// there), plus the 409 S3 answers when two conditional writes of one key
62// race: either way a lock is, or is about to be, present.
63func preconditionFailed(err error) bool {
64 var apiErr smithy.APIError
65 if errors.As(err, &apiErr) {
66 switch apiErr.ErrorCode() {
67 case "PreconditionFailed", "412", "ConditionalRequestConflict":
68 return true
69 }
70 }
71 var opErr *smithy.OperationError
72 if errors.As(err, &opErr) {
73 if h, ok := opErr.Err.(interface{ HTTPStatusCode() int }); ok {
74 return h.HTTPStatusCode() == 412 || h.HTTPStatusCode() == 409
75 }
76 }
77 return false
78}
79