shards: cache ranked slice
On a search request we construct and sort a slice of all shards to
search. This change will cache the slice. We do the caching on the read path
to avoid holding the exclusive lock for a long time when we do writes to the
shard map.
The use of a "rankedVersion" is a bit fiddly. Alternatively we could look
making the replace call a batch version to amortize the cost of creating the
ranked slice.
Change-Id: I5272dbb75a4becebb8aa8727d8459d0666cb9bf9
diff --git a/shards/shards.go b/shards/shards.go
index b90c964..108ba4f 100644
--- a/shards/shards.go
+++ b/shards/shards.go
@@ -45,6 +45,9 @@
capacity int64
shards map[string]rankedShard
+
+ rankedVersion uint64
+ ranked []rankedShard
}
func newShardedSearcher(n int64) *shardedSearcher {
@@ -315,18 +318,33 @@
return s.throttle.Acquire(ctx, 1)
}
-// getShards returns the currently loaded shards. The shards must be
-// accessed under a rlock call. The shards are sorted by decreasing
-// rank.
+// getShards returns the currently loaded shards. The shards must be accessed
+// under a rlock call. The shards are sorted by decreasing rank and should not
+// be mutated.
func (s *shardedSearcher) getShards() []rankedShard {
+ if len(s.ranked) > 0 {
+ return s.ranked
+ }
+
var res []rankedShard
for _, sh := range s.shards {
res = append(res, sh)
}
- // TODO: precompute this.
sort.Slice(res, func(i, j int) bool {
return res[i].rank > res[j].rank
})
+
+ // Cache ranked. We currently hold a read lock, so start a goroutine which
+ // acquires a write lock to update. Use requiredVersion to ensure our
+ // cached slice is still current after acquiring the write lock.
+ go func(ranked []rankedShard, requiredVersion uint64) {
+ s.lock()
+ if s.rankedVersion == requiredVersion {
+ s.ranked = ranked
+ }
+ s.unlock()
+ }(res, s.rankedVersion)
+
return res
}
@@ -376,6 +394,8 @@
Searcher: shard,
}
}
+ s.rankedVersion++
+ s.ranked = nil
}
func loadShard(fn string) (zoekt.Searcher, error) {