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 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "path"
11 "strings"
12 "time"
13
14 awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
15 "github.com/benbjohnson/litestream"
16)
17
18// ErrLockLocation is LockState's answer for a bucket or location it will
19// not read: empty, absolute, or not in clean form.
20var ErrLockLocation = errors.New("lease: invalid lock location")
21
22// lockBodyLimit bounds the lock.json read. The leaser writes a few dozen
23// bytes; anything near this is not a lock.
24const lockBodyLimit = 64 << 10
25
26// LockState reads the single-writer lease at s3://bucket/<location>/lock.json
27// as the litestream leaser writes it (Acquire, lease.go), through the same
28// client Acquire builds, so the read signs for the region and reaches the
29// endpoint the writer used. It is §5.3 step 4's observation: it holds no
30// lease and writes nothing.
31//
32// location is the claim holder's Location from a fresh claim read (the
33// location moves.ReplicaSource derives its prefix from, ruling G3), never
34// activator.HibernatePrefix and never the g-<generation>/ prefix: the lock
35// sits at the location and Held.BindGeneration only fixes the data under
36// it. One trailing slash is accepted, so ReplicaSource's root prefix reads
37// the same lock.
38//
39// held is true while the lock is present and its expiry has not passed by
40// more than RenewMargin, so a holder whose clock runs slightly behind
41// still counts as holding. An absent lock (never acquired, or released:
42// the leaser deletes it) is held=false with a zero owner and expiry and a
43// nil error; only S3's explicit NoSuchKey counts as absent (lockAbsent).
44// An expired lock is held=false with its owner and expiry, for
45// the record. Anything else (a missing bucket, an unreadable or garbled
46// body, a lock with no expiry) is an error, never "not held".
47func LockState(ctx context.Context, bucket, location, region string) (held bool, owner string, expires time.Time, err error) {
48 key, err := lockKey(bucket, location)
49 if err != nil {
50 return false, "", time.Time{}, err
51 }
52 client, _, err := newLeaserClient(ctx, region)
53 if err != nil {
54 return false, "", time.Time{}, err
55 }
56 out, err := client.GetObject(ctx, &awss3.GetObjectInput{Bucket: &bucket, Key: &key})
57 if err != nil {
58 if lockAbsent(err) {
59 return false, "", time.Time{}, nil
60 }
61 return false, "", time.Time{}, fmt.Errorf("lease: read %s: %w", key, err)
62 }
63 defer out.Body.Close()
64 body, err := io.ReadAll(io.LimitReader(out.Body, lockBodyLimit+1))
65 if err != nil {
66 return false, "", time.Time{}, fmt.Errorf("lease: read %s: %w", key, err)
67 }
68 if len(body) > lockBodyLimit {
69 return false, "", time.Time{}, fmt.Errorf("lease: %s is larger than a lock", key)
70 }
71 var lock litestream.Lease
72 if err := json.Unmarshal(body, &lock); err != nil {
73 return false, "", time.Time{}, fmt.Errorf("lease: decode %s: %w", key, err)
74 }
75 if lock.ExpiresAt.IsZero() {
76 return false, "", time.Time{}, fmt.Errorf("lease: %s has no expiry", key)
77 }
78 return lockHeld(lock.ExpiresAt, time.Now()), lock.Owner, lock.ExpiresAt, nil
79}
80
81// lockAbsent reports an explicit S3 object-absence answer: HTTP 404 with an
82// error body the SDK parsed to the code NoSuchKey. Absence is what lets a
83// cut call the lease free, so nothing weaker counts: not a 404 with a
84// malformed, empty or unrecognised body (an endpoint that is not the store,
85// a proxy), not NotFound or NoSuchVersion, not NoSuchBucket, and not
86// NoSuchKey on any other status. mapReadErr is deliberately not used here:
87// its looser reading (any 404 but NoSuchBucket) suits its callers, which
88// treat absence as "nothing to copy", not as authority.
89func lockAbsent(err error) bool {
90 code, status := apiCode(err)
91 return code == "NoSuchKey" && status == http.StatusNotFound
92}
93
94// lockHeld is §5.3 step 4's rule: a lock is released only once its expiry
95// has passed by more than the renewal margin.
96func lockHeld(expires, now time.Time) bool {
97 return !now.After(expires.Add(RenewMargin))
98}
99
100// lockKey is the leaser's key for location (lss3.Leaser.lockKey with Path
101// set, as AcquireTTL sets it), after refusing what Acquire would never be
102// given: an empty bucket or location, an absolute one, or one not in clean
103// form.
104func lockKey(bucket, location string) (string, error) {
105 loc := strings.TrimSuffix(location, "/")
106 if bucket == "" || loc == "" || strings.HasPrefix(loc, "/") || strings.Contains(loc, `\`) || path.Clean(loc) != loc || loc == "." || strings.HasPrefix(loc, "../") || loc == ".." {
107 return "", ErrLockLocation
108 }
109 return loc + "/lock.json", nil
110}
111