Files
tendermint/libs/pubsub/subindex.go
M. J. Fromberger f7f4067968 pubsub: simplify and improve server concurrency handling (#7070)
Rework the internal plumbing of the server. This change does not modify the
exported interfaces or semantics of the package, and all the existing tests
still pass.

The main changes here are to:

- Simplify the interface for subscription indexing with a typed index rather
  than a single nested map.

- Ensure orderly shutdown of channels, so that there is no longer a dynamic
  race with concurrent publishers & subscribers at shutdown.

- Remove a layer of indirection between publishers and subscribers. This mainly
  helps legibility.

- Remove order dependencies between registration and delivery.

- Add documentation comments where they seemed helpful, and clarified the
  existing comments where it was practical.

Although performance was not a primary goal of this change, the simplifications
did very slightly reduce memory use and increase throughput on the existing
benchmarks, though the delta is not statistically significant.

    BENCHMARK                BEFORE AFTER SPEEDUP (%) B/op (B) B/op (A)
    Benchmark10Clients-12    5947   5566  6.4         2017     1942
    Benchmark100Clients-12   6111   5762  5.7         1992     1910
    Benchmark1000Clients-12  6983   6344  9.2         2046     1959
2021-10-19 15:32:13 -07:00

114 lines
3.3 KiB
Go

package pubsub
import "github.com/tendermint/tendermint/abci/types"
// An item to be published to subscribers.
type item struct {
Data interface{}
Events []types.Event
}
// A subInfo value records a single subscription.
type subInfo struct {
clientID string // chosen by the client
query Query // chosen by the client
subID string // assigned at registration
sub *Subscription // receives published events
}
// A subInfoSet is an unordered set of subscription info records.
type subInfoSet map[*subInfo]struct{}
func (s subInfoSet) contains(si *subInfo) bool { _, ok := s[si]; return ok }
func (s subInfoSet) add(si *subInfo) { s[si] = struct{}{} }
func (s subInfoSet) remove(si *subInfo) { delete(s, si) }
// withQuery returns the subset of s whose query string matches qs.
func (s subInfoSet) withQuery(qs string) subInfoSet {
out := make(subInfoSet)
for si := range s {
if si.query.String() == qs {
out.add(si)
}
}
return out
}
// A subIndex is an indexed collection of subscription info records.
// The index is not safe for concurrent use without external synchronization.
type subIndex struct {
all subInfoSet // all subscriptions
byClient map[string]subInfoSet // per-client subscriptions
byQuery map[string]subInfoSet // per-query subscriptions
// TODO(creachadair): We allow indexing by query to support existing use by
// the RPC service methods for event streaming. Fix up those methods not to
// require this, and then remove indexing by query.
}
// newSubIndex constructs a new, empty subscription index.
func newSubIndex() *subIndex {
return &subIndex{
all: make(subInfoSet),
byClient: make(map[string]subInfoSet),
byQuery: make(map[string]subInfoSet),
}
}
// findClients returns the set of subscriptions for the given client ID, or nil.
func (idx *subIndex) findClientID(id string) subInfoSet { return idx.byClient[id] }
// findQuery returns the set of subscriptions on the given query string, or nil.
func (idx *subIndex) findQuery(qs string) subInfoSet { return idx.byQuery[qs] }
// contains reports whether idx contains any subscription matching the given
// client ID and query pair.
func (idx *subIndex) contains(clientID, query string) bool {
csubs, qsubs := idx.byClient[clientID], idx.byQuery[query]
if len(csubs) == 0 || len(qsubs) == 0 {
return false
}
for si := range csubs {
if qsubs.contains(si) {
return true
}
}
return false
}
// add adds si to the index, replacing any previous entry with the same terms.
// It is the caller's responsibility to check for duplicates before adding.
// See also the contains method.
func (idx *subIndex) add(si *subInfo) {
idx.all.add(si)
if m := idx.byClient[si.clientID]; m == nil {
idx.byClient[si.clientID] = subInfoSet{si: struct{}{}}
} else {
m.add(si)
}
qs := si.query.String()
if m := idx.byQuery[qs]; m == nil {
idx.byQuery[qs] = subInfoSet{si: struct{}{}}
} else {
m.add(si)
}
}
// removeAll removes all the elements of s from the index.
func (idx *subIndex) removeAll(s subInfoSet) {
for si := range s {
idx.all.remove(si)
idx.byClient[si.clientID].remove(si)
if len(idx.byClient[si.clientID]) == 0 {
delete(idx.byClient, si.clientID)
}
if si.query != nil {
qs := si.query.String()
idx.byQuery[qs].remove(si)
if len(idx.byQuery[qs]) == 0 {
delete(idx.byQuery, qs)
}
}
}
}