1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
package memdb
import (
"context"
"time"
"github.com/authzed/spicedb/internal/datastore/revisions"
"github.com/authzed/spicedb/pkg/datastore"
)
var ParseRevisionString = revisions.RevisionParser(revisions.Timestamp)
func nowRevision() revisions.TimestampRevision {
return revisions.NewForTime(time.Now().UTC())
}
func (mdb *memdbDatastore) newRevisionID() revisions.TimestampRevision {
mdb.Lock()
defer mdb.Unlock()
existing := mdb.revisions[len(mdb.revisions)-1].revision
created := nowRevision()
// NOTE: The time.Now().UTC() only appears to have *microsecond* level
// precision on macOS Monterey in Go 1.19.1. This means that HeadRevision
// and the result of a ReadWriteTx could return the *same* transaction ID
// if both are executed in sequence without any other forms of delay on
// macOS. We therefore check if the created transaction ID matches that
// previously created and, if not, add to it.
//
// See: https://github.com/golang/go/issues/22037 which appeared to fix
// this in Go 1.9.2, but there appears to have been a reversion with either
// the new version of macOS or Go.
if created.Equal(existing) {
return revisions.NewForTimestamp(created.TimestampNanoSec() + 1)
}
return created
}
func (mdb *memdbDatastore) HeadRevision(_ context.Context) (datastore.Revision, error) {
mdb.RLock()
defer mdb.RUnlock()
if err := mdb.checkNotClosed(); err != nil {
return nil, err
}
return mdb.headRevisionNoLock(), nil
}
func (mdb *memdbDatastore) SquashRevisionsForTesting() {
mdb.revisions = []snapshot{
{
revision: nowRevision(),
db: mdb.db,
},
}
}
func (mdb *memdbDatastore) headRevisionNoLock() revisions.TimestampRevision {
return mdb.revisions[len(mdb.revisions)-1].revision
}
func (mdb *memdbDatastore) OptimizedRevision(_ context.Context) (datastore.Revision, error) {
mdb.RLock()
defer mdb.RUnlock()
if err := mdb.checkNotClosed(); err != nil {
return nil, err
}
now := nowRevision()
return revisions.NewForTimestamp(now.TimestampNanoSec() - now.TimestampNanoSec()%mdb.quantizationPeriod), nil
}
func (mdb *memdbDatastore) CheckRevision(_ context.Context, dr datastore.Revision) error {
mdb.RLock()
defer mdb.RUnlock()
if err := mdb.checkNotClosed(); err != nil {
return err
}
return mdb.checkRevisionLocalCallerMustLock(dr)
}
func (mdb *memdbDatastore) checkRevisionLocalCallerMustLock(dr datastore.Revision) error {
now := nowRevision()
// Ensure the revision has not fallen outside of the GC window. If it has, it is considered
// invalid.
if mdb.revisionOutsideGCWindow(now, dr) {
return datastore.NewInvalidRevisionErr(dr, datastore.RevisionStale)
}
// If the revision <= now and later than the GC window, it is assumed to be valid, even if
// HEAD revision is behind it.
if dr.GreaterThan(now) {
// If the revision is in the "future", then check to ensure that it is <= of HEAD to handle
// the microsecond granularity on macos (see comment above in newRevisionID)
headRevision := mdb.headRevisionNoLock()
if dr.LessThan(headRevision) || dr.Equal(headRevision) {
return nil
}
return datastore.NewInvalidRevisionErr(dr, datastore.CouldNotDetermineRevision)
}
return nil
}
func (mdb *memdbDatastore) revisionOutsideGCWindow(now revisions.TimestampRevision, revisionRaw datastore.Revision) bool {
// make an exception for head revision - it will be acceptable even if outside GC Window
if revisionRaw.Equal(mdb.headRevisionNoLock()) {
return false
}
oldest := revisions.NewForTimestamp(now.TimestampNanoSec() + mdb.negativeGCWindow)
return revisionRaw.LessThan(oldest)
}
|