Skip to content

Commit e2e5895

Browse files
committed
fix: Invalidate the cache after write. Moved the db copy from the persist label to the manager.
1 parent 03eb6eb commit e2e5895

10 files changed

Lines changed: 382 additions & 108 deletions

File tree

‎pkg/publicshare/manager/json/json.go‎

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,6 @@ func New(gwAddr string, pwHashCost, janitorRunInterval int, enableCleanup bool,
123123
return m, nil
124124
}
125125

126-
var _ publicshare.ClosableManager = (*manager)(nil)
127-
128126
type commonConfig struct {
129127
GatewayAddr string `mapstructure:"gateway_addr"`
130128
SharePasswordHashCost int `mapstructure:"password_hash_cost"`
@@ -152,7 +150,7 @@ func (c *commonConfig) init() {
152150
c.SharePasswordHashCost = 11
153151
}
154152
if c.JanitorRunInterval == 0 {
155-
c.JanitorRunInterval = 600
153+
c.JanitorRunInterval = 3600 // 1 hour
156154
}
157155
}
158156

@@ -174,6 +172,8 @@ type manager struct {
174172
janitorDone chan struct{}
175173
}
176174

175+
var _ publicshare.ClosableManager = (*manager)(nil)
176+
177177
// init is called at the top of every public method to lazily initialize the
178178
// persistence layer. It must not take m.mutex: persistence.Init is already
179179
// idempotent and self-synchronized (it returns immediately once the
@@ -262,19 +262,20 @@ func (m *manager) Load(ctx context.Context, shareChan <-chan *publicshare.WithPa
262262
if err != nil {
263263
return err
264264
}
265+
dbCopy := persistence.Copy(db)
265266

266267
for ps := range shareChan {
267268
encShare, err := utils.MarshalProtoV1ToJSON(&ps.PublicShare)
268269
if err != nil {
269270
return err
270271
}
271272

272-
db[ps.PublicShare.Id.GetOpaqueId()] = map[string]interface{}{
273+
dbCopy[ps.PublicShare.Id.GetOpaqueId()] = map[string]interface{}{
273274
"share": string(encShare),
274275
"password": ps.Password,
275276
}
276277
}
277-
return m.persistence.Write(ctx, db)
278+
return m.persistence.Write(ctx, dbCopy)
278279
}
279280

280281
// CreatePublicShare adds a new entry to manager.shares
@@ -345,17 +346,18 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr
345346
if err != nil {
346347
return nil, err
347348
}
349+
dbCopy := persistence.Copy(db)
348350

349-
if _, ok := db[s.Id.GetOpaqueId()]; !ok {
350-
db[s.Id.GetOpaqueId()] = map[string]interface{}{
351+
if _, ok := dbCopy[s.Id.GetOpaqueId()]; !ok {
352+
dbCopy[s.Id.GetOpaqueId()] = map[string]interface{}{
351353
"share": string(encShare),
352354
"password": ps.Password,
353355
}
354356
} else {
355357
return nil, errors.New("key already exists")
356358
}
357359

358-
err = m.persistence.Write(ctx, db)
360+
err = m.persistence.Write(ctx, dbCopy)
359361
if err != nil {
360362
return nil, err
361363
}
@@ -433,13 +435,14 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link
433435
if err != nil {
434436
return nil, err
435437
}
438+
dbCopy := persistence.Copy(db)
436439

437440
encShare, err := utils.MarshalProtoV1ToJSON(share)
438441
if err != nil {
439442
return nil, err
440443
}
441444

442-
data, ok := db[share.Id.OpaqueId].(map[string]interface{})
445+
data, ok := dbCopy[share.Id.OpaqueId].(map[string]interface{})
443446
if !ok {
444447
data = map[string]interface{}{}
445448
}
@@ -449,9 +452,9 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link
449452
}
450453
data["share"] = string(encShare)
451454

452-
db[share.Id.OpaqueId] = data
455+
dbCopy[share.Id.OpaqueId] = data
453456

454-
err = m.persistence.Write(ctx, db)
457+
err = m.persistence.Write(ctx, dbCopy)
455458
if err != nil {
456459
return nil, err
457460
}
@@ -522,10 +525,9 @@ func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []
522525

523526
m.mutex.RLock()
524527

525-
// Read returns a copy that shares no mutable state with the persistence
526-
// backend (see persistence.Copy), so it's safe to keep using db after
527-
// the lock is released below - a concurrent writer can no longer race
528-
// what we do with it.
528+
// Ranging over db below happens after the lock is released, which is safe
529+
// because we never mutate it: writers copy before they mutate, and the
530+
// persistence layer publishes a new map instead of changing this one.
529531
db, err := m.persistence.Read(ctx)
530532
if err != nil {
531533
m.mutex.RUnlock()
@@ -627,10 +629,11 @@ func (m *manager) cleanupExpiredShares() error {
627629
return err
628630
}
629631

630-
db, err := m.persistence.Read(ctx)
632+
read, err := m.persistence.Read(ctx)
631633
if err != nil {
632634
return err
633635
}
636+
db := persistence.Copy(read)
634637

635638
var changed bool
636639
for id, v := range db {
@@ -642,9 +645,6 @@ func (m *manager) cleanupExpiredShares() error {
642645
}
643646

644647
if publicshare.IsExpired(&ps) {
645-
// db is our own copy (see persistence.Copy), so deleting the
646-
// current entry while ranging over it is safe: single goroutine,
647-
// no aliasing with the persistence backend's internal state.
648648
delete(db, id)
649649
changed = true
650650
}
@@ -671,10 +671,11 @@ func (m *manager) RevokePublicShare(ctx context.Context, _ *user.User, ref *link
671671

672672
// revokePublicShare doesn't have a lock inside, ensure a lock before call
673673
func (m *manager) revokePublicShare(ctx context.Context, ref *link.PublicShareReference) error {
674-
db, err := m.persistence.Read(ctx)
674+
read, err := m.persistence.Read(ctx)
675675
if err != nil {
676676
return err
677677
}
678+
db := persistence.Copy(read)
678679

679680
switch {
680681
case ref.GetId() != nil && ref.GetId().OpaqueId != "":

‎pkg/publicshare/manager/json/json_test.go‎

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,67 @@ import (
4343
. "github.com/onsi/gomega"
4444
)
4545

46-
// slowReadPersistence is a fake persistence.Persistence whose Read blocks
47-
// for a fixed delay, standing in for a network round trip such as the cs3
48-
// persistence layer's Stat/SimpleDownload against metadata.CS3. It has no
49-
// state of its own to protect - it exists to let a test observe whether
50-
// concurrent manager calls overlap during that delay.
51-
type slowReadPersistence struct {
52-
delay time.Duration
46+
// barrierPersistence is a fake persistence.Persistence standing in for one
47+
// whose Read is a network round trip, such as the cs3 layer's Stat and
48+
// SimpleDownload against metadata.CS3. Its Read parks until the expected number
49+
// of callers are inside it at once and records how many ever were, which turns
50+
// "do these calls overlap?" into a counter the test can assert on rather than
51+
// an elapsed time it has to interpret. A loaded CI runner slows every caller
52+
// equally and so cannot change the answer.
53+
type barrierPersistence struct {
54+
expected int
55+
56+
mu sync.Mutex
57+
concurrent int
58+
maxConcurrent int
59+
60+
releaseOnce sync.Once
61+
release chan struct{}
5362
}
5463

55-
func (p *slowReadPersistence) Init(_ context.Context) error { return nil }
64+
func newBarrierPersistence(expected int) *barrierPersistence {
65+
return &barrierPersistence{expected: expected, release: make(chan struct{})}
66+
}
67+
68+
func (p *barrierPersistence) Init(_ context.Context) error { return nil }
69+
70+
func (p *barrierPersistence) Read(_ context.Context) (persistence.PublicShares, error) {
71+
p.mu.Lock()
72+
p.concurrent++
73+
if p.concurrent > p.maxConcurrent {
74+
p.maxConcurrent = p.concurrent
75+
}
76+
if p.concurrent == p.expected {
77+
p.releaseOnce.Do(func() { close(p.release) })
78+
}
79+
p.mu.Unlock()
80+
81+
select {
82+
case <-p.release:
83+
case <-time.After(10 * time.Second):
84+
// Nobody joined us, so the callers are being serialized somewhere above.
85+
// Open the gate for good, or every queued caller would wait out its own
86+
// timeout and the test would take expected*timeout to report it.
87+
p.releaseOnce.Do(func() { close(p.release) })
88+
}
89+
90+
p.mu.Lock()
91+
p.concurrent--
92+
p.mu.Unlock()
5693

57-
func (p *slowReadPersistence) Read(_ context.Context) (persistence.PublicShares, error) {
58-
time.Sleep(p.delay)
5994
return persistence.PublicShares{}, nil
6095
}
6196

62-
func (p *slowReadPersistence) Write(_ context.Context, _ persistence.PublicShares) error {
97+
func (p *barrierPersistence) Write(_ context.Context, _ persistence.PublicShares) error {
6398
return nil
6499
}
65100

101+
func (p *barrierPersistence) peakConcurrency() int {
102+
p.mu.Lock()
103+
defer p.mu.Unlock()
104+
return p.maxConcurrent
105+
}
106+
66107
var _ = Describe("Json", func() {
67108
var (
68109
user1 = &userpb.User{
@@ -360,17 +401,15 @@ var _ = Describe("Json", func() {
360401
// whichever call was already inside persistence.Read - silently
361402
// undoing the switch from sync.Mutex to sync.RWMutex. A wrong
362403
// re-introduction of that lock wouldn't fail -race (it's a
363-
// correctly-used lock), only show up as this test timing out.
364-
const (
365-
delay = 150 * time.Millisecond
366-
concurrency = 8
367-
)
404+
// correctly-used lock), only show up here as the reads no longer
405+
// managing to be inside persistence.Read at the same time.
406+
const concurrency = 8
368407

369-
slow, err := json.New("https://localhost:9200", 11, 60, false, &slowReadPersistence{delay: delay})
408+
barrier := newBarrierPersistence(concurrency)
409+
slow, err := json.New("https://localhost:9200", 11, 60, false, barrier)
370410
Expect(err).ToNot(HaveOccurred())
371411

372412
var wg sync.WaitGroup
373-
start := time.Now()
374413
wg.Add(concurrency)
375414
for range concurrency {
376415
go func() {
@@ -380,11 +419,9 @@ var _ = Describe("Json", func() {
380419
}
381420
wg.Wait()
382421

383-
// Fully serialized would take concurrency*delay (1.2s here).
384-
// Overlapping reads should finish close to a single delay - allow
385-
// generous slack for scheduling noise without letting a real
386-
// regression pass.
387-
Expect(time.Since(start)).To(BeNumerically("<", delay*3))
422+
Expect(barrier.peakConcurrency()).To(Equal(concurrency),
423+
"only %d of %d ListPublicShares calls were ever inside persistence.Read together, so they are being serialized",
424+
barrier.peakConcurrency(), concurrency)
388425
})
389426

390427
It("refreshes its cache before writing new data", func() {

‎pkg/publicshare/manager/json/persistence/cs3/cs3.go‎

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func (p *cs3) Read(ctx context.Context) (persistence.PublicShares, error) {
101101
info, err := p.s.Stat(ctx, "publicshares.json")
102102
if err != nil {
103103
if _, ok := err.(errtypes.NotFound); ok {
104-
return persistence.Copy(p.db.publicShares), nil // Nothing to sync against
104+
return p.db.publicShares, nil
105105
}
106106
return nil, err
107107
}
@@ -117,7 +117,7 @@ func (p *cs3) Read(ctx context.Context) (persistence.PublicShares, error) {
117117
}
118118
p.db.mtime = utils.TSToTime(info.Mtime)
119119
}
120-
return persistence.Copy(p.db.publicShares), nil
120+
return p.db.publicShares, nil
121121
}
122122

123123
func (p *cs3) Write(ctx context.Context, db persistence.PublicShares) error {
@@ -142,17 +142,9 @@ func (p *cs3) Write(ctx context.Context, db persistence.PublicShares) error {
142142
return err
143143
}
144144

145-
// Keep the cache in sync with what was just persisted. This used to
146-
// happen implicitly, because Read() handed out a reference to
147-
// p.db.publicShares itself and callers mutated it in place before
148-
// calling Write() with that same map. Now that Read() returns an
149-
// independent copy (see persistence.Copy), it has to be done explicitly
150-
// here, or the cache would only pick up our own write once some later
151-
// external write advances the remote mtime past our stale one.
152-
if info, statErr := p.s.Stat(ctx, "publicshares.json"); statErr == nil {
153-
p.db.mtime = utils.TSToTime(info.Mtime)
154-
}
155-
p.db.publicShares = persistence.Copy(db)
145+
// Invalidate the cache to minimize the risk of inconsistency.
146+
p.db.mtime = time.Time{}
147+
p.db.publicShares = persistence.PublicShares{}
156148

157149
return nil
158150
}

0 commit comments

Comments
 (0)