From 86b9591c381c99390fdbfc84d2dd73cd02ae3e44 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Wed, 23 Oct 2024 02:40:02 +0400 Subject: [PATCH 01/17] sync2: improve rangesync Receive semantics Given that after recent item sync is done (if it's needed at all), the range set reconciliation algorithm no longer depends on newly received item being added to the set, we can save memory by not adding the received items during reconciliation. During real sync, the received items will be sent to the respective handlers and after the corresponding data are fetched and validated, they will be added to the database, without the need to add them to cloned OrderedSets which are used to sync against particular peers. --- dev-docs/sync2-set-reconciliation.md | 17 ++- sync2/rangesync/dumbset.go | 162 ++++++++++++++++++--------- sync2/rangesync/export_test.go | 4 +- sync2/rangesync/fingerprint.go | 6 + sync2/rangesync/fingerprint_test.go | 5 + sync2/rangesync/interface.go | 12 +- sync2/rangesync/keybytes.go | 19 ++++ sync2/rangesync/keybytes_test.go | 65 ++++++++++- sync2/rangesync/message.go | 8 +- sync2/rangesync/p2p_test.go | 63 ++++------- sync2/rangesync/rangesync.go | 71 +++++++++--- sync2/rangesync/rangesync_test.go | 106 ++++-------------- sync2/rangesync/wire_conduit.go | 20 +++- sync2/rangesync/wire_conduit_test.go | 24 ++-- sync2/rangesync/wire_types.go | 19 ++-- 15 files changed, 372 insertions(+), 229 deletions(-) diff --git a/dev-docs/sync2-set-reconciliation.md b/dev-docs/sync2-set-reconciliation.md index bce54c6e64..a67db60196 100644 --- a/dev-docs/sync2-set-reconciliation.md +++ b/dev-docs/sync2-set-reconciliation.md @@ -533,18 +533,24 @@ This message is used for [Recent sync](#recent-sync). It is sent as the initial message when recent sync is enabled. `Recent` message is preceded by a number of `ItemChunk` messages -carrying the actual items (keys). +carrying the actual items (keys). The items in immediately preceding +`ItemChunk` message must be added to the set immediately before +proceeding with further reconciliation. Parameters: * `SinceTime`: nanoseconds since Unix epoch marking the beginning of recent items that were sent, according to the local timestamp. + If `SinceTime` is zero, this indicates a response to another Recent + message. As a response to `Recent` message, the local items starting from `SinceTime` according to their local timestamp are sent to the peer -via a number of `ItemChunk` messages. After that the sync sequence -without further `Recent` message but with or without MinHash probing -is initiated, depending on whether a positive `maxDiff` value is set -via the `WithMaxDiff` option. +via a number of `ItemChunk` messages, followed by `Recent` message +with zero timestamp, indicating that the items need to be added to the +set immediately before proceeding with further reconciliation. After +that the sync sequence without further `Recent` message but with or +without MinHash probing is initiated, depending on whether a positive +`maxDiff` value is set via the `WithMaxDiff` option. ```mermaid sequenceDiagram @@ -557,6 +563,7 @@ sequenceDiagram loop B->>A: ItemChunk
(Items) end + B->>A: Recent
(Since=0) Note over B: The sample is taken
after consuming
the recent items from A B->>A: Sample
(X, Y, Count, Fingerprint, Items) ``` diff --git a/sync2/rangesync/dumbset.go b/sync2/rangesync/dumbset.go index b2d54cdc80..03cdaf67d6 100644 --- a/sync2/rangesync/dumbset.go +++ b/sync2/rangesync/dumbset.go @@ -81,7 +81,7 @@ func naiveRange( } } -var naiveFPFunc = func(items []KeyBytes) Fingerprint { +func naiveFPFunc(items []KeyBytes) Fingerprint { s := "" for _, k := range items { s += string(k) @@ -89,22 +89,44 @@ var naiveFPFunc = func(items []KeyBytes) Fingerprint { return stringToFP(s) } -// dumbSet is a simple OrderedSet implementation that doesn't include any optimizations. +func realFPFunc(items []KeyBytes) Fingerprint { + hasher := hash.GetHasher() + defer func() { + hasher.Reset() + hash.PutHasher(hasher) + }() + for _, h := range items { + hasher.Write(h[:]) + } + var hashRes [32]byte + hasher.Sum(hashRes[:0]) + var r Fingerprint + copy(r[:], hashRes[:]) + return r +} + +// DumbSet is a simple OrderedSet implementation that doesn't include any optimizations. // It is intended to be only used in tests. -type dumbSet struct { - keys []KeyBytes - DisableReAdd bool - added map[string]bool - fpFunc func(items []KeyBytes) Fingerprint +type DumbSet struct { + keys []KeyBytes + received map[string]int + added map[string]bool + allowMutiReceive bool + FPFunc func(items []KeyBytes) Fingerprint } -var _ OrderedSet = &dumbSet{} +var _ OrderedSet = &DumbSet{} -// Receive implements the OrderedSet. -func (ds *dumbSet) Receive(id KeyBytes) error { +// SetAllowMultiReceive sets whether the set allows receiving the same item multiple times. +func (ds *DumbSet) SetAllowMultiReceive(allow bool) { + ds.allowMutiReceive = allow +} + +// AddUnchecked adds an item to the set without registerting the item for checks +// as in case of Add and Receive. +func (ds *DumbSet) AddUnchecked(id KeyBytes) { if len(ds.keys) == 0 { ds.keys = []KeyBytes{id} - return nil } p := slices.IndexFunc(ds.keys, func(other KeyBytes) bool { return other.Compare(id) >= 0 @@ -113,25 +135,75 @@ func (ds *dumbSet) Receive(id KeyBytes) error { case p < 0: ds.keys = append(ds.keys, id) case id.Compare(ds.keys[p]) == 0: - if ds.DisableReAdd { - if ds.added[string(id)] { - panic("hash sent twice: " + id.String()) - } - if ds.added == nil { - ds.added = make(map[string]bool) - } - ds.added[string(id)] = true - } // already present default: ds.keys = slices.Insert(ds.keys, p, id) } +} + +// AddReceived adds all the received items to the set. +func (ds *DumbSet) AddReceived() { + sr := ds.Received() + for k := range sr.Seq { + ds.AddUnchecked(KeyBytes(k)) + } + // DumbSet's Received implementation should never return an error + if sr.Error() != nil { + panic("unexpected error in Received") + } +} + +// Add implements the OrderedSet. +func (ds *DumbSet) Add(id KeyBytes) error { + if ds.added == nil { + ds.added = make(map[string]bool) + } + sid := string(id) + if ds.added[sid] { + panic("item already added via Add: " + id.String()) + } + ds.added[sid] = true + // Add is invoked during recent sync. + // If the item was already received, this means it was received as part of the + // recent sync, and thus may be received again as the algorithm does not guarantee + // that items already in the set are not received from the remote peer, only that + // no item is received twice (with exception of recent sync). + if ds.received[sid] != 0 { + ds.received[sid] = 0 + } + ds.AddUnchecked(id) + return nil +} +// Receive implements the OrderedSet. +func (ds *DumbSet) Receive(id KeyBytes) error { + if ds.received == nil { + ds.received = make(map[string]int) + } + sid := string(id) + ds.received[sid]++ + if !ds.allowMutiReceive && ds.received[sid] > 1 { + panic("item already received: " + id.String()) + } return nil } +// Received implements the OrderedSet. +func (ds *DumbSet) Received() SeqResult { + return SeqResult{ + Seq: func(yield func(KeyBytes) bool) { + for k := range ds.received { + if !yield(KeyBytes(k)) { + break + } + } + }, + Error: NoSeqError, + } +} + // seq returns an endless sequence as a SeqResult starting from the given index. -func (ds *dumbSet) seq(n int) SeqResult { +func (ds *DumbSet) seq(n int) SeqResult { if n < 0 || n > len(ds.keys) { panic("bad index") } @@ -151,7 +223,7 @@ func (ds *dumbSet) seq(n int) SeqResult { // seqFor returns an endless sequence as a SeqResult starting from the given key, or the // lowest key greater than the given key if the key is not present in the set. -func (ds *dumbSet) seqFor(s KeyBytes) SeqResult { +func (ds *DumbSet) seqFor(s KeyBytes) SeqResult { n := slices.IndexFunc(ds.keys, func(k KeyBytes) bool { return k.Compare(s) == 0 }) @@ -161,7 +233,7 @@ func (ds *dumbSet) seqFor(s KeyBytes) SeqResult { return ds.seq(n) } -func (ds *dumbSet) getRangeInfo( +func (ds *DumbSet) getRangeInfo( x, y KeyBytes, count int, ) (r RangeInfo, end KeyBytes, err error) { @@ -177,9 +249,9 @@ func (ds *dumbSet) getRangeInfo( panic("BUG: bad X or Y") } rangeItems, start, end := naiveRange(ds.keys, x, y, count) - fpFunc := ds.fpFunc + fpFunc := ds.FPFunc if fpFunc == nil { - fpFunc = naiveFPFunc + fpFunc = realFPFunc } r = RangeInfo{ Fingerprint: fpFunc(rangeItems), @@ -197,13 +269,13 @@ func (ds *dumbSet) getRangeInfo( } // GetRangeInfo implements OrderedSet. -func (ds *dumbSet) GetRangeInfo(x, y KeyBytes, count int) (RangeInfo, error) { - ri, _, err := ds.getRangeInfo(x, y, count) +func (ds *DumbSet) GetRangeInfo(x, y KeyBytes) (RangeInfo, error) { + ri, _, err := ds.getRangeInfo(x, y, -1) return ri, err } // SplitRange implements OrderedSet. -func (ds *dumbSet) SplitRange(x, y KeyBytes, count int) (SplitInfo, error) { +func (ds *DumbSet) SplitRange(x, y KeyBytes, count int) (SplitInfo, error) { if count <= 0 { panic("BUG: bad split count") } @@ -225,12 +297,12 @@ func (ds *dumbSet) SplitRange(x, y KeyBytes, count int) (SplitInfo, error) { } // Empty implements OrderedSet. -func (ds *dumbSet) Empty() (bool, error) { +func (ds *DumbSet) Empty() (bool, error) { return len(ds.keys) == 0, nil } // Items implements OrderedSet. -func (ds *dumbSet) Items() SeqResult { +func (ds *DumbSet) Items() SeqResult { if len(ds.keys) == 0 { return EmptySeqResult() } @@ -238,33 +310,13 @@ func (ds *dumbSet) Items() SeqResult { } // Copy implements OrderedSet. -func (ds *dumbSet) Copy(syncScope bool) OrderedSet { - return &dumbSet{keys: slices.Clone(ds.keys)} +func (ds *DumbSet) Copy(syncScope bool) OrderedSet { + return &DumbSet{ + keys: slices.Clone(ds.keys), + } } // Recent implements OrderedSet. -func (ds *dumbSet) Recent(since time.Time) (SeqResult, int) { +func (ds *DumbSet) Recent(since time.Time) (SeqResult, int) { return EmptySeqResult(), 0 } - -// NewDumbSet creates a new dumbSet instance. -// If disableReAdd is true, the set will panic if the same item is received twice. -func NewDumbSet(disableReAdd bool) OrderedSet { - return &dumbSet{ - DisableReAdd: disableReAdd, - fpFunc: func(items []KeyBytes) (r Fingerprint) { - hasher := hash.GetHasher() - defer func() { - hasher.Reset() - hash.PutHasher(hasher) - }() - var hashRes [32]byte - for _, h := range items { - hasher.Write(h[:]) - } - hasher.Sum(hashRes[:0]) - copy(r[:], hashRes[:]) - return r - }, - } -} diff --git a/sync2/rangesync/export_test.go b/sync2/rangesync/export_test.go index ade03c9be0..b55abbfa27 100644 --- a/sync2/rangesync/export_test.go +++ b/sync2/rangesync/export_test.go @@ -4,11 +4,11 @@ var ( StartWireConduit = startWireConduit StringToFP = stringToFP CHash = chash + NaiveFPFunc = naiveFPFunc ) type ( - Sender = sender - DumbSet = dumbSet + Sender = sender ) func (rsr *RangeSetReconciler) DoRound(s Sender) (done bool, err error) { return rsr.doRound(s) } diff --git a/sync2/rangesync/fingerprint.go b/sync2/rangesync/fingerprint.go index 1d5e7e9e01..2e2a372afd 100644 --- a/sync2/rangesync/fingerprint.go +++ b/sync2/rangesync/fingerprint.go @@ -46,6 +46,12 @@ func (fp *Fingerprint) BitFromLeft(i int) bool { return fp[bi]&(0x1< FingerprintSize { + panic("BUG: bad fingerprint bit index") + } + return k[bi]&(0x1<= len(k) { + return + } + clear(k[bi+1:]) + k[bi] &^= 0xff >> uint(bit%8) +} + // RandomKeyBytes generates random data in bytes for testing. func RandomKeyBytes(size int) KeyBytes { b := make([]byte, size) diff --git a/sync2/rangesync/keybytes_test.go b/sync2/rangesync/keybytes_test.go index c061c01cbe..a1d9fb2e7d 100644 --- a/sync2/rangesync/keybytes_test.go +++ b/sync2/rangesync/keybytes_test.go @@ -22,6 +22,16 @@ func TestKeyBytes(t *testing.T) { require.Equal(t, -1, a.Compare(b)) require.Equal(t, 1, b.Compare(a)) require.Equal(t, 0, a.Compare(a)) + var bits []bool + for i := 0; i < 16; i++ { + bits = append(bits, b.BitFromLeft(i)) + } + require.Equal(t, []bool{ + false, false, false, false, + false, false, false, true, + false, false, false, false, + false, false, true, false, + }, bits) c := rangesync.RandomKeyBytes(6) require.Len(t, c, 6) @@ -30,7 +40,7 @@ func TestKeyBytes(t *testing.T) { require.NotEqual(t, c, d) } -func TestIncID(t *testing.T) { +func TestIncKeyBytes(t *testing.T) { for _, tc := range []struct { id, expected string overflow bool @@ -57,3 +67,56 @@ func TestIncID(t *testing.T) { require.Equal(t, expected, id) } } + +func TestTrimKeyBytes(t *testing.T) { + for _, tc := range []struct { + k, expected string + bit int + }{ + { + k: "010203040506", + expected: "010203040506", + bit: 48, + }, + { + k: "010203040506", + expected: "010203040506", + bit: 128, + }, + { + k: "a10203040506", + expected: "a00000000000", + bit: 4, + }, + { + k: "a1b203040506", + expected: "a1b000000000", + bit: 12, + }, + { + k: "a1b203040506", + expected: "a1b200000000", + bit: 16, + }, + { + k: "a1b203040506", + expected: "a1b203000000", + bit: 24, + }, + { + k: "a1b203040506", + expected: "a1b203000000", + bit: 28, + }, + { + k: "a1b203040506", + expected: "a1b203040000", + bit: 32, + }, + } { + k := rangesync.MustParseHexKeyBytes(tc.k) + k.Trim(tc.bit) + expected := rangesync.MustParseHexKeyBytes(tc.expected) + require.Equal(t, expected, k) + } +} diff --git a/sync2/rangesync/message.go b/sync2/rangesync/message.go index ff82921975..b7d854e735 100644 --- a/sync2/rangesync/message.go +++ b/sync2/rangesync/message.go @@ -167,9 +167,11 @@ func (s sender) SendSample( } func (s sender) SendRecent(since time.Time) error { - return s.Send(&RecentMessage{ - SinceTime: uint64(since.UnixNano()), - }) + var m RecentMessage + if !since.IsZero() { + m.SinceTime = uint64(since.UnixNano()) + } + return s.Send(&m) } // "Empty" message types follow. These do not need scalegen and thus are not in wire_types.go. diff --git a/sync2/rangesync/p2p_test.go b/sync2/rangesync/p2p_test.go index 0c1dcb11b8..2032a63c39 100644 --- a/sync2/rangesync/p2p_test.go +++ b/sync2/rangesync/p2p_test.go @@ -2,6 +2,8 @@ package rangesync_test import ( "context" + "slices" + "sync/atomic" "testing" "time" @@ -50,14 +52,14 @@ func newClientServerTester( return &cst, ctx } -func fakeRequesterGetter() getRequesterFunc { +func fakeRequesterGetter(t *testing.T) getRequesterFunc { return func( name string, handler server.StreamHandler, peers ...rangesync.Requester, ) (rangesync.Requester, p2p.Peer) { pid := p2p.Peer(name) - return newFakeRequester(pid, handler, peers...), pid + return newFakeRequester(t, pid, handler, peers...), pid } } @@ -92,7 +94,7 @@ func p2pRequesterGetter(t *testing.T) getRequesterFunc { } type syncTracer struct { - dumb bool + dumb atomic.Bool receivedItems int sentItems int } @@ -100,7 +102,7 @@ type syncTracer struct { var _ rangesync.Tracer = &syncTracer{} func (tr *syncTracer) OnDumbSync() { - tr.dumb = true + tr.dumb.Store(true) } func (tr *syncTracer) OnRecent(receivedItems, sentItems int) { @@ -152,19 +154,11 @@ func (frs *fakeRecentSet) Recent(since time.Time) (rangesync.SeqResult, int) { if err != nil { return rangesync.ErrorSeqResult(err), 0 } - for _, k := range items { - if !frs.timestamps[string(k)].Before(since) { - items = append(items, k) - } - } + items = slices.DeleteFunc(items, func(k rangesync.KeyBytes) bool { + return frs.timestamps[string(k)].Before(since) + }) return rangesync.SeqResult{ - Seq: func(yield func(rangesync.KeyBytes) bool) { - for _, h := range items { - if !yield(h) { - return - } - } - }, + Seq: rangesync.Seq(slices.Values(items)), Error: rangesync.NoSeqError, }, len(items) } @@ -212,7 +206,6 @@ func testWireSync(t *testing.T, getRequester getRequesterFunc) { maxNumSpecificA: 500, minNumSpecificB: 400, maxNumSpecificB: 500, - allowReAdd: true, }, dumb: false, opts: []rangesync.RangeSetReconcilerOption{ @@ -254,11 +247,13 @@ func testWireSync(t *testing.T, getRequester getRequesterFunc) { pss := rangesync.NewPairwiseSetSyncer(cst.client, "test", opts, nil) err := pss.Sync(ctx, cst.srvPeerID, setB, nil, nil) require.NoError(t, err) + st.setA.AddReceived() + st.setB.AddReceived() t.Logf("numSpecific: %d, bytesSent %d, bytesReceived %d", st.numSpecificA+st.numSpecificB, cst.pss.Sent(), cst.pss.Received()) - require.Equal(t, tc.dumb, tr.dumb, "dumb sync") + require.Equal(t, tc.dumb, tr.dumb.Load(), "dumb sync") require.Equal(t, tc.receivedRecent, tr.receivedItems > 0) require.Equal(t, tc.sentRecent, tr.sentItems > 0) st.verify(st.setA, st.setB) @@ -268,7 +263,7 @@ func testWireSync(t *testing.T, getRequester getRequesterFunc) { func TestWireSync(t *testing.T) { t.Run("fake requester", func(t *testing.T) { - testWireSync(t, fakeRequesterGetter()) + testWireSync(t, fakeRequesterGetter(t)) }) t.Run("p2p", func(t *testing.T) { testWireSync(t, p2pRequesterGetter(t)) @@ -287,9 +282,9 @@ func testWireProbe(t *testing.T, getRequester getRequesterFunc) { cst, ctx := newClientServerTester(t, st.setA, getRequester, st.opts, nil) pss := rangesync.NewPairwiseSetSyncer(cst.client, "test", st.opts, nil) itemsA := st.setA.Items() - kA, err := itemsA.First() + x, err := itemsA.First() require.NoError(t, err) - infoA, err := st.setA.GetRangeInfo(kA, kA, -1) + infoA, err := st.setA.GetRangeInfo(x, x) require.NoError(t, err) prA, err := pss.Probe(ctx, cst.srvPeerID, st.setB, nil, nil) require.NoError(t, err) @@ -297,34 +292,18 @@ func testWireProbe(t *testing.T, getRequester getRequesterFunc) { require.Equal(t, infoA.Count, prA.Count) require.InDelta(t, 0.98, prA.Sim, 0.05, "sim") - itemsA = st.setA.Items() + splitInfo, err := st.setA.SplitRange(x, x, infoA.Count/2) require.NoError(t, err) - kA, err = itemsA.First() - require.NoError(t, err) - partInfoA, err := st.setA.GetRangeInfo(kA, kA, infoA.Count/2) - require.NoError(t, err) - x, err := partInfoA.Items.First() - require.NoError(t, err) - var y rangesync.KeyBytes - n := partInfoA.Count + 1 - for k := range partInfoA.Items.Seq { - y = k - n-- - if n == 0 { - break - } - } - require.NoError(t, partInfoA.Items.Error()) - prA, err = pss.Probe(ctx, cst.srvPeerID, st.setB, x, y) + prA, err = pss.Probe(ctx, cst.srvPeerID, st.setB, x, splitInfo.Middle) require.NoError(t, err) - require.Equal(t, partInfoA.Fingerprint, prA.FP) - require.Equal(t, partInfoA.Count, prA.Count) + require.Equal(t, splitInfo.Parts[0].Fingerprint, prA.FP) + require.Equal(t, splitInfo.Parts[0].Count, prA.Count) require.InDelta(t, 0.98, prA.Sim, 0.1, "sim") } func TestWireProbe(t *testing.T) { t.Run("fake requester", func(t *testing.T) { - testWireProbe(t, fakeRequesterGetter()) + testWireProbe(t, fakeRequesterGetter(t)) }) t.Run("p2p", func(t *testing.T) { testWireProbe(t, p2pRequesterGetter(t)) diff --git a/sync2/rangesync/rangesync.go b/sync2/rangesync/rangesync.go index ab62b658ed..2843080657 100644 --- a/sync2/rangesync/rangesync.go +++ b/sync2/rangesync/rangesync.go @@ -343,6 +343,43 @@ func (rsr *RangeSetReconciler) messageRange( return x, y, nil } +func (rsr *RangeSetReconciler) handleRecent( + s sender, + msg SyncMessage, + x, y KeyBytes, + receivedKeys map[string]struct{}, +) error { + since := msg.Since() + if since.IsZero() { + // This is a response to a Recent message with timestamp. + // It is only needed so that we add the received items to the set + // immediately, which is already done above. + return nil + } + + sr, count := rsr.os.Recent(msg.Since()) + nSent := 0 + if count != 0 { + // Do not send back recent items that were received + var err error + if nSent, err = rsr.sendItems(s, count, sr, receivedKeys); err != nil { + return err + } + } + // Following the items, we send Recent message with zero time. + // The peer will see this as the indicator that it needs to add the + // received items to its set immediately, before proceeding with further + // reconciliation steps. + if err := s.SendRecent(time.Time{}); err != nil { + return fmt.Errorf("sending recent: %w", err) + } + rsr.log.Debug("handled recent message", + zap.Int("receivedCount", len(receivedKeys)), + zap.Int("sentCount", nSent)) + rsr.tracer.OnRecent(len(receivedKeys), nSent) + return rsr.initiate(s, x, y, false) +} + // handleMessage handles incoming messages. Note that the set reconciliation protocol is // designed to be stateless. func (rsr *RangeSetReconciler) handleMessage( @@ -357,6 +394,18 @@ func (rsr *RangeSetReconciler) handleMessage( return false, err } + if msg.Type() == MessageTypeRecent { + for k := range receivedKeys { + // Add received items to the set. Receive() was already + // called on these items, but we also need them to + // be present in the set before we proceed with further + // reconciliation. + if err := rsr.os.Add(KeyBytes(k)); err != nil { + return false, fmt.Errorf("adding an item to the set: %w", err) + } + } + } + if x == nil { switch msg.Type() { case MessageTypeProbe: @@ -367,12 +416,12 @@ func (rsr *RangeSetReconciler) handleMessage( return false, err } case MessageTypeRecent: - rsr.tracer.OnRecent(len(receivedKeys), 0) + return false, rsr.handleRecent(s, msg, x, y, receivedKeys) } return true, nil } - info, err := rsr.os.GetRangeInfo(x, y, -1) + info, err := rsr.os.GetRangeInfo(x, y) if err != nil { return false, err } @@ -423,19 +472,7 @@ func (rsr *RangeSetReconciler) handleMessage( return true, nil case MessageTypeRecent: - sr, count := rsr.os.Recent(msg.Since()) - nSent := 0 - if count != 0 { - // Do not send back recent items that were received - if nSent, err = rsr.sendItems(s, count, sr, receivedKeys); err != nil { - return false, err - } - } - rsr.log.Debug("handled recent message", - zap.Int("receivedCount", len(receivedKeys)), - zap.Int("sentCount", nSent)) - rsr.tracer.OnRecent(len(receivedKeys), nSent) - return false, rsr.initiate(s, x, y, false) + return false, rsr.handleRecent(s, msg, x, y, receivedKeys) case MessageTypeFingerprint, MessageTypeSample: return rsr.handleFingerprint(s, msg, x, y, info) @@ -472,7 +509,7 @@ func (rsr *RangeSetReconciler) initiate(s sender, x, y KeyBytes, haveRecent bool rsr.log.Debug("initiate: send empty set") return s.SendEmptySet() } - info, err := rsr.os.GetRangeInfo(x, y, -1) + info, err := rsr.os.GetRangeInfo(x, y) if err != nil { return fmt.Errorf("get range info: %w", err) } @@ -526,7 +563,7 @@ func (rsr *RangeSetReconciler) InitiateProbe( x, y KeyBytes, ) (RangeInfo, error) { s := sender{c} - info, err := rsr.os.GetRangeInfo(x, y, -1) + info, err := rsr.os.GetRangeInfo(x, y) if err != nil { return RangeInfo{}, err } diff --git a/sync2/rangesync/rangesync_test.go b/sync2/rangesync/rangesync_test.go index 8d2862e7b1..ed93c415d3 100644 --- a/sync2/rangesync/rangesync_test.go +++ b/sync2/rangesync/rangesync_test.go @@ -53,12 +53,14 @@ func (fc *fakeConduit) Send(msg rangesync.SyncMessage) error { return nil } -func makeSet(t *testing.T, items string) *rangesync.DumbSet { - var s rangesync.DumbSet +func makeSet(items string) *rangesync.DumbSet { + s := &rangesync.DumbSet{ + FPFunc: rangesync.NaiveFPFunc, + } for _, c := range []byte(items) { - require.NoError(t, s.Receive(rangesync.KeyBytes{c})) + s.AddUnchecked(rangesync.KeyBytes{c}) } - return &s + return s } func setStr(os rangesync.OrderedSet) string { @@ -85,7 +87,7 @@ func dumpRangeMessages(t *testing.T, msgs []rangesync.SyncMessage, fmt string, a } t.Logf(fmt, args...) for _, m := range msgs { - t.Logf(" %s", m) + t.Logf(" %s", rangesync.SyncMessageToString(m)) } } @@ -293,14 +295,12 @@ func TestRangeSync(t *testing.T) { logger := zaptest.NewLogger(t) for n, maxSendRange := range []int{1, 2, 3, 4} { t.Logf("maxSendRange: %d", maxSendRange) - setA := makeSet(t, tc.a) - setA.DisableReAdd = true + setA := makeSet(tc.a) syncA := rangesync.NewRangeSetReconciler(setA, rangesync.WithLogger(logger.Named("A")), rangesync.WithMaxSendRange(maxSendRange), rangesync.WithItemChunkSize(3)) - setB := makeSet(t, tc.b) - setB.DisableReAdd = true + setB := makeSet(tc.b) syncB := rangesync.NewRangeSetReconciler(setB, rangesync.WithLogger(logger.Named("B")), rangesync.WithMaxSendRange(maxSendRange), @@ -320,6 +320,8 @@ func TestRangeSync(t *testing.T) { nRounds, _, _ = runSync(t, syncA, syncB, x, y, tc.maxRounds[n]) t.Logf("%s: maxSendRange %d: %d rounds", tc.name, maxSendRange, nRounds) + setA.AddReceived() + setB.AddReceived() require.Equal(t, tc.countA, prBA.Count, "countA") require.Equal(t, tc.countB, prAB.Count, "countB") @@ -352,14 +354,14 @@ func TestRandomSync(t *testing.T) { bytesA[i], bytesA[j] = bytesA[j], bytesA[i] }) bytesA = bytesA[:rand.Intn(len(bytesA))] - setA := makeSet(t, string(bytesA)) + setA := makeSet(string(bytesA)) bytesB = append([]byte(nil), chars...) rand.Shuffle(len(bytesB), func(i, j int) { bytesB[i], bytesB[j] = bytesB[j], bytesB[i] }) bytesB = bytesB[:rand.Intn(len(bytesB))] - setB := makeSet(t, string(bytesB)) + setB := makeSet(string(bytesB)) keySet := make(map[byte]struct{}) for _, c := range append(bytesA, bytesB...) { @@ -378,6 +380,8 @@ func TestRandomSync(t *testing.T) { rangesync.WithItemChunkSize(3)) runSync(t, syncA, syncB, nil, nil, max(len(expectedSet), 2)) + setA.AddReceived() + setB.AddReceived() require.Equal(t, setStr(setA), setStr(setB)) require.Equal(t, string(expectedSet), setStr(setA), "expected set for %q<->%q", bytesA, bytesB) @@ -391,13 +395,12 @@ type hashSyncTestConfig struct { maxNumSpecificA int minNumSpecificB int maxNumSpecificB int - allowReAdd bool } type hashSyncTester struct { t *testing.T src []rangesync.KeyBytes - setA, setB rangesync.OrderedSet + setA, setB *rangesync.DumbSet opts []rangesync.RangeSetReconcilerOption numSpecificA int numSpecificB int @@ -421,16 +424,16 @@ func newHashSyncTester(t *testing.T, cfg hashSyncTestConfig) *hashSyncTester { } sliceA := st.src[:cfg.numTestHashes-st.numSpecificB] - st.setA = rangesync.NewDumbSet(!cfg.allowReAdd) + st.setA = &rangesync.DumbSet{} for _, h := range sliceA { - require.NoError(t, st.setA.Receive(h)) + st.setA.AddUnchecked(h) } sliceB := slices.Clone(st.src[:cfg.numTestHashes-st.numSpecificB-st.numSpecificA]) sliceB = append(sliceB, st.src[cfg.numTestHashes-st.numSpecificB:]...) - st.setB = rangesync.NewDumbSet(!cfg.allowReAdd) + st.setB = &rangesync.DumbSet{} for _, h := range sliceB { - require.NoError(t, st.setB.Receive(h)) + st.setB.AddUnchecked(h) } slices.SortFunc(st.src, func(a, b rangesync.KeyBytes) int { @@ -465,72 +468,7 @@ func TestSyncHash(t *testing.T) { itemCoef := float64(nItems) / float64(numSpecific) t.Logf("numSpecific: %d, nRounds: %d, nMsg: %d, nItems: %d, itemCoef: %.2f", numSpecific, nRounds, nMsg, nItems, itemCoef) + st.setA.AddReceived() + st.setB.AddReceived() st.verify(st.setA, st.setB) } - -// deferredAddSet wraps an OrderedSet and defers actually adding items until addAll() is -// called. This is used to check that the set reconciliation algorithm, except for the -// Recent sync part, doesn't depend on items being added to the set immediately. -type deferredAddSet struct { - rangesync.OrderedSet - added map[string]struct{} -} - -// Receive implements the OrderedSet. -func (das *deferredAddSet) Receive(id rangesync.KeyBytes) error { - if das.added == nil { - das.added = make(map[string]struct{}) - } - das.added[string(id)] = struct{}{} - return nil -} - -// addAll adds all deferred items to the underlying OrderedSet. -func (das *deferredAddSet) addAll() error { - for k := range das.added { - if err := das.OrderedSet.Receive(rangesync.KeyBytes(k)); err != nil { - return err - } - } - return nil -} - -func TestDeferredAdd(t *testing.T) { - st := newHashSyncTester(t, hashSyncTestConfig{ - maxSendRange: 1, - numTestHashes: 10000, - minNumSpecificA: 4, - maxNumSpecificA: 90, - minNumSpecificB: 4, - maxNumSpecificB: 90, - }) - opts := append(st.opts, rangesync.WithMaxDiff(0.9)) - var msgLists [][]rangesync.SyncMessage - - sync := func(setA, setB rangesync.OrderedSet) { - syncA := rangesync.NewRangeSetReconciler(setA, opts...) - syncB := rangesync.NewRangeSetReconciler(setB, opts...) - fc := &fakeConduit{t: t} - require.NoError(t, syncA.Initiate(fc, nil, nil)) - nRounds, nMsg, nItems := doRunSync(fc, syncA, syncB, 100) - numSpecific := st.numSpecificA + st.numSpecificB - itemCoef := float64(nItems) / float64(numSpecific) - t.Logf("numSpecific: %d, nRounds: %d, nMsg: %d, nItems: %d, itemCoef: %.2f", - numSpecific, nRounds, nMsg, nItems, itemCoef) - msgLists = append(msgLists, fc.rec) - } - - setA := st.setA.Copy(true) - setB := st.setB.Copy(true) - sync(setA, setB) - st.verify(setA, setB) - - dSetA := &deferredAddSet{OrderedSet: st.setA.Copy(true)} - dSetB := &deferredAddSet{OrderedSet: st.setB.Copy(true)} - sync(dSetA, dSetB) - dSetA.addAll() - dSetB.addAll() - st.verify(dSetA, dSetB) - - require.Equal(t, msgLists[0], msgLists[1]) -} diff --git a/sync2/rangesync/wire_conduit.go b/sync2/rangesync/wire_conduit.go index 6c79188b3c..ffde72aed3 100644 --- a/sync2/rangesync/wire_conduit.go +++ b/sync2/rangesync/wire_conduit.go @@ -13,7 +13,23 @@ import ( ) const ( - writeQueueSize = 10000 + // TODO: currently, in RangeSetReconciler, the reconciliation process may block + // indefinitely if the send queue in wireConduit overflows, causing connection to + // time out, after which reconciliation is interrupted. + // A way to partly mitigate this issue would be the following: + // 1. Invoking Receive() immediately on any incoming set items. It may help that + // the set is only actually modified via Add() when handling items associated + // with Recent messages. + // 2. Branch out into more goroutines when handling incoming messages upon sending + // being blocked. This way, we'll allow the remote side to receive some + // messages by handling the messages it sent us and unblocking its send queue. + // The OrderedSet is only added to by RangeSetReconciler when receiving recent + // items. Receive() semantics should be updated so that Receive() being called on + // OrderedSet's copies' does the same as Receive() being called on the original + // OrderedSet. After these changes, it should be easy enough to parallelize + // RangeSetReconciler's message handling as needed, passing copies of OrderedSet + // to the new goroutines. + sendQueueSize = 200000 ) var ErrLimitExceeded = errors.New("sync traffic/message limit exceeded") @@ -58,7 +74,7 @@ var _ Conduit = &wireConduit{} func startWireConduit(ctx context.Context, s io.ReadWriter, opts ...ConduitOption) *wireConduit { c := &wireConduit{ stream: s, - sendCh: make(chan SyncMessage, writeQueueSize), + sendCh: make(chan SyncMessage, sendQueueSize), stopCh: make(chan struct{}), } for _, opt := range opts { diff --git a/sync2/rangesync/wire_conduit_test.go b/sync2/rangesync/wire_conduit_test.go index ac52495d31..f4909e8a79 100644 --- a/sync2/rangesync/wire_conduit_test.go +++ b/sync2/rangesync/wire_conduit_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" @@ -31,6 +32,7 @@ type incomingRequest struct { } type fakeRequester struct { + t *testing.T id p2p.Peer handler server.StreamHandler peers map[p2p.Peer]*fakeRequester @@ -39,8 +41,14 @@ type fakeRequester struct { var _ rangesync.Requester = &fakeRequester{} -func newFakeRequester(id p2p.Peer, handler server.StreamHandler, peers ...rangesync.Requester) *fakeRequester { +func newFakeRequester( + t *testing.T, + id p2p.Peer, + handler server.StreamHandler, + peers ...rangesync.Requester, +) *fakeRequester { fr := &fakeRequester{ + t: t, id: id, handler: handler, reqCh: make(chan incomingRequest), @@ -65,7 +73,7 @@ func (fr *fakeRequester) Run(ctx context.Context) error { case req = <-fr.reqCh: } if err := fr.handler(ctx, req.initialRequest, req.stream); err != nil { - panic("handler error: " + err.Error()) + assert.Fail(fr.t, "handler error: %v", err) } } } @@ -129,7 +137,7 @@ func TestWireConduit(t *testing.T) { } fp := rangesync.Fingerprint(hs[2][:12]) srv := newFakeRequester( - "srv", + t, "srv", func(ctx context.Context, initialRequest []byte, stream io.ReadWriter) error { require.Equal(t, []byte("hello"), initialRequest) c := rangesync.StartWireConduit(ctx, stream) @@ -163,7 +171,7 @@ func TestWireConduit(t *testing.T) { runRequester(t, srv) - client := newFakeRequester("client", nil, srv) + client := newFakeRequester(t, "client", nil, srv) require.NoError(t, client.StreamRequest(context.Background(), "srv", []byte("hello"), func(ctx context.Context, stream io.ReadWriter) error { c := rangesync.StartWireConduit(ctx, stream) @@ -227,7 +235,7 @@ func TestWireConduit_Limits(t *testing.T) { t.Run(tc.name, func(t *testing.T) { errCh := make(chan error) srv := newFakeRequester( - "srv", + t, "srv", func(ctx context.Context, initialRequest []byte, stream io.ReadWriter) error { c := rangesync.StartWireConduit(ctx, stream, tc.opts...) defer c.Stop() @@ -248,7 +256,7 @@ func TestWireConduit_Limits(t *testing.T) { runRequester(t, srv) - client := newFakeRequester("client", nil, srv) + client := newFakeRequester(t, "client", nil, srv) var eg errgroup.Group ctx, cancel := context.WithCancel(context.Background()) defer func() { @@ -285,7 +293,7 @@ func TestWireConduit_Limits(t *testing.T) { func TestWireConduit_StopSend(t *testing.T) { started := make(chan struct{}) srv := newFakeRequester( - "srv", + t, "srv", func(ctx context.Context, initialRequest []byte, stream io.ReadWriter) error { close(started) // This will hang @@ -295,7 +303,7 @@ func TestWireConduit_StopSend(t *testing.T) { runRequester(t, srv) - client := newFakeRequester("client", nil, srv) + client := newFakeRequester(t, "client", nil, srv) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() client.StreamRequest(ctx, "srv", []byte("hello"), diff --git a/sync2/rangesync/wire_types.go b/sync2/rangesync/wire_types.go index 3ccfa64f5d..6c9cfa4716 100644 --- a/sync2/rangesync/wire_types.go +++ b/sync2/rangesync/wire_types.go @@ -126,11 +126,16 @@ type RecentMessage struct { var _ SyncMessage = &RecentMessage{} -func (m *RecentMessage) Type() MessageType { return MessageTypeRecent } -func (m *RecentMessage) X() KeyBytes { return nil } -func (m *RecentMessage) Y() KeyBytes { return nil } -func (m *RecentMessage) Fingerprint() Fingerprint { return EmptyFingerprint() } -func (m *RecentMessage) Count() int { return 0 } -func (m *RecentMessage) Keys() []KeyBytes { return nil } -func (m *RecentMessage) Since() time.Time { return time.Unix(0, int64(m.SinceTime)) } +func (m *RecentMessage) Type() MessageType { return MessageTypeRecent } +func (m *RecentMessage) X() KeyBytes { return nil } +func (m *RecentMessage) Y() KeyBytes { return nil } +func (m *RecentMessage) Fingerprint() Fingerprint { return EmptyFingerprint() } +func (m *RecentMessage) Count() int { return 0 } +func (m *RecentMessage) Keys() []KeyBytes { return nil } +func (m *RecentMessage) Since() time.Time { + if m.SinceTime == 0 { + return time.Time{} + } + return time.Unix(0, int64(m.SinceTime)) +} func (m *RecentMessage) Sample() []MinhashSampleItem { return nil } From ef30f479266bef0103a6fe5d6dc9554db7694f99 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Wed, 23 Oct 2024 14:28:27 +0400 Subject: [PATCH 02/17] sync2: implement multi-peer synchronization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds multi-peer synchronization support. When the local set differs too much from the remote sets, "torrent-style" "split sync" is attempted which splits the set into subranges and syncs each sub-range against a separate peer. Otherwise, the full sync is done, syncing the whole set against each of the synchronization peers. Full sync is also done after each split sync run. The local set can be considered synchronized after the specified number of full syncs has happened. The approach is loosely based on [SREP: Out-Of-Band Sync of Transaction Pools for Large-Scale Blockchains](https://people.bu.edu/staro/2023-ICBC-Novak.pdf) paper by Novak Boškov, Sevval Simsek, Ari Trachtenberg, and David Starobinski. --- fetch/peers/peers.go | 7 + p2p/server/server.go | 23 +- p2p/server/server_test.go | 16 +- sync2/multipeer/delim.go | 22 + sync2/multipeer/delim_test.go | 105 +++ sync2/multipeer/dumbset.go | 58 ++ sync2/multipeer/export_test.go | 25 + sync2/multipeer/interface.go | 85 ++ sync2/multipeer/mocks_test.go | 1239 +++++++++++++++++++++++++++ sync2/multipeer/multipeer.go | 463 ++++++++++ sync2/multipeer/multipeer_test.go | 376 ++++++++ sync2/multipeer/setsyncbase.go | 182 ++++ sync2/multipeer/setsyncbase_test.go | 273 ++++++ sync2/multipeer/split_sync.go | 206 +++++ sync2/multipeer/split_sync_test.go | 150 ++++ sync2/multipeer/sync_queue.go | 108 +++ sync2/multipeer/sync_queue_test.go | 70 ++ sync2/multipeer/synclist.go | 60 ++ sync2/multipeer/synclist_test.go | 35 + sync2/p2p.go | 211 +++++ sync2/p2p_test.go | 162 ++++ sync2/rangesync/p2p.go | 13 +- 22 files changed, 3877 insertions(+), 12 deletions(-) create mode 100644 sync2/multipeer/delim.go create mode 100644 sync2/multipeer/delim_test.go create mode 100644 sync2/multipeer/dumbset.go create mode 100644 sync2/multipeer/export_test.go create mode 100644 sync2/multipeer/interface.go create mode 100644 sync2/multipeer/mocks_test.go create mode 100644 sync2/multipeer/multipeer.go create mode 100644 sync2/multipeer/multipeer_test.go create mode 100644 sync2/multipeer/setsyncbase.go create mode 100644 sync2/multipeer/setsyncbase_test.go create mode 100644 sync2/multipeer/split_sync.go create mode 100644 sync2/multipeer/split_sync_test.go create mode 100644 sync2/multipeer/sync_queue.go create mode 100644 sync2/multipeer/sync_queue_test.go create mode 100644 sync2/multipeer/synclist.go create mode 100644 sync2/multipeer/synclist_test.go create mode 100644 sync2/p2p.go create mode 100644 sync2/p2p_test.go diff --git a/fetch/peers/peers.go b/fetch/peers/peers.go index 38a9b61d9d..91d581c474 100644 --- a/fetch/peers/peers.go +++ b/fetch/peers/peers.go @@ -54,6 +54,13 @@ type Peers struct { globalLatency float64 } +func (p *Peers) Contains(id peer.ID) bool { + p.mu.Lock() + defer p.mu.Unlock() + _, exist := p.peers[id] + return exist +} + func (p *Peers) Add(id peer.ID) bool { p.mu.Lock() defer p.mu.Unlock() diff --git a/p2p/server/server.go b/p2p/server/server.go index dedbd618e7..d9ae545b9c 100644 --- a/p2p/server/server.go +++ b/p2p/server/server.go @@ -104,12 +104,30 @@ func WithRequestsPerInterval(n int, interval time.Duration) Opt { } } +// WithDecayingTag specifies P2P decaying tag that is applied to the peer when a request +// is being served. func WithDecayingTag(tag DecayingTagSpec) Opt { return func(s *Server) { s.decayingTagSpec = &tag } } +type peerIDKey struct{} + +func withPeerID(ctx context.Context, peerID peer.ID) context.Context { + return context.WithValue(ctx, peerIDKey{}, peerID) +} + +// ContextPeerID retrieves the ID of the peer being served from the context and a boolean +// value indicating that the context contains peer ID. If there's no peer ID associated +// with the context, the function returns an empty peer ID and false. +func ContextPeerID(ctx context.Context) (peer.ID, bool) { + if v := ctx.Value(peerIDKey{}); v != nil { + return v.(peer.ID), true + } + return peer.ID(""), false +} + // Handler is a handler to be defined by the application. type Handler func(context.Context, []byte) ([]byte, error) @@ -264,7 +282,8 @@ func (s *Server) Run(ctx context.Context) error { eg.Wait() return nil } - ctx, cancel := context.WithCancel(ctx) + peer := req.stream.Conn().RemotePeer() + ctx, cancel := context.WithCancel(withPeerID(ctx, peer)) eg.Go(func() error { <-ctx.Done() s.sem.Release(1) @@ -275,7 +294,7 @@ func (s *Server) Run(ctx context.Context) error { defer cancel() conn := req.stream.Conn() if s.decayingTag != nil { - s.decayingTag.Bump(conn.RemotePeer(), s.decayingTagSpec.Inc) + s.decayingTag.Bump(peer, s.decayingTagSpec.Inc) } ok := s.queueHandler(ctx, req.stream) duration := time.Since(req.received) diff --git a/p2p/server/server_test.go b/p2p/server/server_test.go index e0b8693225..c6eab164b9 100644 --- a/p2p/server/server_test.go +++ b/p2p/server/server_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/spacemeshos/go-scale/tester" "github.com/stretchr/testify/assert" @@ -45,8 +46,10 @@ func TestServer(t *testing.T) { request := []byte("test request") testErr := errors.New("test error") - handler := func(_ context.Context, msg []byte) ([]byte, error) { - return msg, nil + handler := func(ctx context.Context, msg []byte) ([]byte, error) { + peerID, found := ContextPeerID(ctx) + require.True(t, found) + return append(msg, []byte(peerID)...), nil } errhandler := func(_ context.Context, _ []byte) ([]byte, error) { return nil, testErr @@ -81,6 +84,9 @@ func TestServer(t *testing.T) { append(opts, WithRequestSizeLimit(limit))..., ) ctx, cancel := context.WithCancel(context.Background()) + noPeerID, found := ContextPeerID(ctx) + require.Equal(t, peer.ID(""), noPeerID) + require.False(t, found) var eg errgroup.Group eg.Go(func() error { return srv1.Run(ctx) @@ -109,7 +115,8 @@ func TestServer(t *testing.T) { srvID := mesh.Hosts()[1].ID() response, err := client.Request(ctx, srvID, request) require.NoError(t, err) - require.Equal(t, request, response) + expResponse := append(request, []byte(mesh.Hosts()[0].ID())...) + require.Equal(t, expResponse, response) srvConns := mesh.Hosts()[1].Network().ConnsToPeer(mesh.Hosts()[0].ID()) require.NotEmpty(t, srvConns) require.Equal(t, n+1, srv1.NumAcceptedRequests()) @@ -129,7 +136,8 @@ func TestServer(t *testing.T) { srvID := mesh.Hosts()[3].ID() response, err := client.Request(ctx, srvID, request) require.NoError(t, err) - require.Equal(t, request, response) + expResponse := append(request, []byte(mesh.Hosts()[0].ID())...) + require.Equal(t, expResponse, response) srvConns := mesh.Hosts()[3].Network().ConnsToPeer(mesh.Hosts()[0].ID()) require.NotEmpty(t, srvConns) require.Equal(t, n+1, srv1.NumAcceptedRequests()) diff --git a/sync2/multipeer/delim.go b/sync2/multipeer/delim.go new file mode 100644 index 0000000000..73da72c2c2 --- /dev/null +++ b/sync2/multipeer/delim.go @@ -0,0 +1,22 @@ +package multipeer + +import ( + "encoding/binary" + + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +func getDelimiters(numPeers, keyLen, maxDepth int) (h []rangesync.KeyBytes) { + if numPeers < 2 { + return nil + } + mask := uint64(0xffffffffffffffff) << (64 - maxDepth) + inc := (uint64(0x80) << 56) / uint64(numPeers) + h = make([]rangesync.KeyBytes, numPeers-1) + for i, v := 0, uint64(0); i < numPeers-1; i++ { + h[i] = make(rangesync.KeyBytes, keyLen) + v += inc + binary.BigEndian.PutUint64(h[i], (v<<1)&mask) + } + return h +} diff --git a/sync2/multipeer/delim_test.go b/sync2/multipeer/delim_test.go new file mode 100644 index 0000000000..bcee897a77 --- /dev/null +++ b/sync2/multipeer/delim_test.go @@ -0,0 +1,105 @@ +package multipeer_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/sync2/multipeer" +) + +func TestGetDelimiters(t *testing.T) { + for _, tc := range []struct { + numPeers int + keyLen int + maxDepth int + values []string + }{ + { + numPeers: 0, + maxDepth: 64, + keyLen: 32, + values: nil, + }, + { + numPeers: 1, + maxDepth: 64, + keyLen: 32, + values: nil, + }, + { + numPeers: 2, + maxDepth: 64, + keyLen: 32, + values: []string{ + "8000000000000000000000000000000000000000000000000000000000000000", + }, + }, + { + numPeers: 2, + maxDepth: 24, + keyLen: 32, + values: []string{ + "8000000000000000000000000000000000000000000000000000000000000000", + }, + }, + { + numPeers: 3, + maxDepth: 64, + keyLen: 32, + values: []string{ + "5555555555555554000000000000000000000000000000000000000000000000", + "aaaaaaaaaaaaaaa8000000000000000000000000000000000000000000000000", + }, + }, + { + numPeers: 3, + maxDepth: 24, + keyLen: 32, + values: []string{ + "5555550000000000000000000000000000000000000000000000000000000000", + "aaaaaa0000000000000000000000000000000000000000000000000000000000", + }, + }, + { + numPeers: 3, + maxDepth: 4, + keyLen: 32, + values: []string{ + "5000000000000000000000000000000000000000000000000000000000000000", + "a000000000000000000000000000000000000000000000000000000000000000", + }, + }, + { + numPeers: 4, + maxDepth: 64, + keyLen: 32, + values: []string{ + "4000000000000000000000000000000000000000000000000000000000000000", + "8000000000000000000000000000000000000000000000000000000000000000", + "c000000000000000000000000000000000000000000000000000000000000000", + }, + }, + { + numPeers: 4, + maxDepth: 24, + keyLen: 32, + values: []string{ + "4000000000000000000000000000000000000000000000000000000000000000", + "8000000000000000000000000000000000000000000000000000000000000000", + "c000000000000000000000000000000000000000000000000000000000000000", + }, + }, + } { + ds := multipeer.GetDelimiters(tc.numPeers, tc.keyLen, tc.maxDepth) + var hs []string + for _, d := range ds { + hs = append(hs, d.String()) + } + if len(tc.values) == 0 { + require.Empty(t, hs, "%d delimiters", tc.numPeers) + } else { + require.Equal(t, tc.values, hs, "%d delimiters", tc.numPeers) + } + } +} diff --git a/sync2/multipeer/dumbset.go b/sync2/multipeer/dumbset.go new file mode 100644 index 0000000000..31f37c6c0e --- /dev/null +++ b/sync2/multipeer/dumbset.go @@ -0,0 +1,58 @@ +package multipeer + +import ( + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +// DumbSet is an unoptimized OrderedSet to be used for testing purposes. +// It builds on rangesync.DumbSet. +type DumbSet struct { + *rangesync.DumbSet +} + +var _ OrderedSet = &DumbSet{} + +// NewDumbHashSet creates an unoptimized OrderedSet to be used for testing purposes. +// If disableReAdd is true, receiving the same item multiple times will fail. +func NewDumbHashSet() *DumbSet { + return &DumbSet{ + DumbSet: &rangesync.DumbSet{}, + } +} + +// Advance implements OrderedSet. +func (ds *DumbSet) EnsureLoaded() error { + return nil +} + +// Advance implements OrderedSet. +func (ds *DumbSet) Advance() error { + return nil +} + +// Has implements OrderedSet. +func (ds *DumbSet) Has(k rangesync.KeyBytes) (bool, error) { + var first rangesync.KeyBytes + sr := ds.Items() + for cur := range sr.Seq { + if first == nil { + first = cur + } else if first.Compare(cur) == 0 { + return false, sr.Error() + } + if k.Compare(cur) == 0 { + return true, sr.Error() + } + } + return false, sr.Error() +} + +// Copy implements OrderedSet. +func (ds *DumbSet) Copy(syncScope bool) rangesync.OrderedSet { + return &DumbSet{ds.DumbSet.Copy(syncScope).(*rangesync.DumbSet)} +} + +// Release implements OrderedSet. +func (ds *DumbSet) Release() error { + return nil +} diff --git a/sync2/multipeer/export_test.go b/sync2/multipeer/export_test.go new file mode 100644 index 0000000000..3b6dcbb328 --- /dev/null +++ b/sync2/multipeer/export_test.go @@ -0,0 +1,25 @@ +package multipeer + +import ( + "context" + + "github.com/spacemeshos/go-spacemesh/p2p" +) + +type ( + SyncRunner = syncRunner + SplitSync = splitSync +) + +var ( + WithSyncRunner = withSyncRunner + WithClock = withClock + GetDelimiters = getDelimiters + NewSyncQueue = newSyncQueue + NewSplitSync = newSplitSync + NewSyncList = newSyncList +) + +func (mpr *MultiPeerReconciler) FullSync(ctx context.Context, syncPeers []p2p.Peer) error { + return mpr.fullSync(ctx, syncPeers) +} diff --git a/sync2/multipeer/interface.go b/sync2/multipeer/interface.go new file mode 100644 index 0000000000..43e1ce2177 --- /dev/null +++ b/sync2/multipeer/interface.go @@ -0,0 +1,85 @@ +package multipeer + +import ( + "context" + "io" + + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +//go:generate mockgen -typed -package=multipeer_test -destination=./mocks_test.go -source=./interface.go + +// OrdredSet is an interface for a set that can be synced against a remote peer. +// It extends rangesync.OrderedSet with methods which are needed for multi-peer +// reconciliation. +type OrderedSet interface { + rangesync.OrderedSet + // EnsureLoaded ensures that the set is loaded and ready for use. + // It may do nothing in case of in-memory sets, but may trigger loading + // from database in case of database-backed sets. + EnsureLoaded() error + // Advance advances the set by including the items since the set was last loaded + // or advanced. + Advance() error + // Has returns true if the specified key is present in OrderedSet. + Has(rangesync.KeyBytes) (bool, error) + // Release releases the resources associated with the set. + // Calling Release on a set that is already released is a no-op. + Release() error +} + +// SyncBase is a synchronization base which holds the original OrderedSet. +type SyncBase interface { + // Count returns the number of items in the set. + Count() (int, error) + // Derive creates a Syncer for the specified peer. + Derive(p p2p.Peer) Syncer + // Probe probes the specified peer, obtaining its set fingerprint, + // the number of items and the similarity value. + Probe(ctx context.Context, p p2p.Peer) (rangesync.ProbeResult, error) + // Wait waits for all the derived syncers' handlers to finish. + Wait() error +} + +// Syncer is a synchronization interface for a single peer. +type Syncer interface { + // Peer returns the peer this syncer is for. + Peer() p2p.Peer + // Sync synchronizes the set with the peer. + Sync(ctx context.Context, x, y rangesync.KeyBytes) error + // Serve serves a synchronization request on the specified stream. + Serve(ctx context.Context, stream io.ReadWriter) error + // Release releases the resources associated with the syncer. + // Calling Release on a syncer that is already released is a no-op. + Release() error +} + +// SyncKeyHandler is a handler for keys that are received from peers. +type SyncKeyHandler interface { + // Receive handles a key that was received from a peer. + Receive(k rangesync.KeyBytes, peer p2p.Peer) (bool, error) + // Commit is invoked at the end of synchronization to apply the changes. + Commit(peer p2p.Peer, base, new OrderedSet) error +} + +type PairwiseSyncer interface { + Probe( + ctx context.Context, + peer p2p.Peer, + os rangesync.OrderedSet, + x, y rangesync.KeyBytes, + ) (rangesync.ProbeResult, error) + Sync( + ctx context.Context, + peer p2p.Peer, + os rangesync.OrderedSet, + x, y rangesync.KeyBytes, + ) error + Serve(context context.Context, stream io.ReadWriter, os rangesync.OrderedSet) error +} + +type syncRunner interface { + SplitSync(ctx context.Context, syncPeers []p2p.Peer) error + FullSync(ctx context.Context, syncPeers []p2p.Peer) error +} diff --git a/sync2/multipeer/mocks_test.go b/sync2/multipeer/mocks_test.go new file mode 100644 index 0000000000..50fc483615 --- /dev/null +++ b/sync2/multipeer/mocks_test.go @@ -0,0 +1,1239 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./interface.go +// +// Generated by this command: +// +// mockgen -typed -package=multipeer_test -destination=./mocks_test.go -source=./interface.go +// + +// Package multipeer_test is a generated GoMock package. +package multipeer_test + +import ( + context "context" + io "io" + reflect "reflect" + time "time" + + p2p "github.com/spacemeshos/go-spacemesh/p2p" + multipeer "github.com/spacemeshos/go-spacemesh/sync2/multipeer" + rangesync "github.com/spacemeshos/go-spacemesh/sync2/rangesync" + gomock "go.uber.org/mock/gomock" +) + +// MockOrderedSet is a mock of OrderedSet interface. +type MockOrderedSet struct { + ctrl *gomock.Controller + recorder *MockOrderedSetMockRecorder + isgomock struct{} +} + +// MockOrderedSetMockRecorder is the mock recorder for MockOrderedSet. +type MockOrderedSetMockRecorder struct { + mock *MockOrderedSet +} + +// NewMockOrderedSet creates a new mock instance. +func NewMockOrderedSet(ctrl *gomock.Controller) *MockOrderedSet { + mock := &MockOrderedSet{ctrl: ctrl} + mock.recorder = &MockOrderedSetMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOrderedSet) EXPECT() *MockOrderedSetMockRecorder { + return m.recorder +} + +// Add mocks base method. +func (m *MockOrderedSet) Add(k rangesync.KeyBytes) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Add", k) + ret0, _ := ret[0].(error) + return ret0 +} + +// Add indicates an expected call of Add. +func (mr *MockOrderedSetMockRecorder) Add(k any) *MockOrderedSetAddCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockOrderedSet)(nil).Add), k) + return &MockOrderedSetAddCall{Call: call} +} + +// MockOrderedSetAddCall wrap *gomock.Call +type MockOrderedSetAddCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetAddCall) Return(arg0 error) *MockOrderedSetAddCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetAddCall) Do(f func(rangesync.KeyBytes) error) *MockOrderedSetAddCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetAddCall) DoAndReturn(f func(rangesync.KeyBytes) error) *MockOrderedSetAddCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Advance mocks base method. +func (m *MockOrderedSet) Advance() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Advance") + ret0, _ := ret[0].(error) + return ret0 +} + +// Advance indicates an expected call of Advance. +func (mr *MockOrderedSetMockRecorder) Advance() *MockOrderedSetAdvanceCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Advance", reflect.TypeOf((*MockOrderedSet)(nil).Advance)) + return &MockOrderedSetAdvanceCall{Call: call} +} + +// MockOrderedSetAdvanceCall wrap *gomock.Call +type MockOrderedSetAdvanceCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetAdvanceCall) Return(arg0 error) *MockOrderedSetAdvanceCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetAdvanceCall) Do(f func() error) *MockOrderedSetAdvanceCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetAdvanceCall) DoAndReturn(f func() error) *MockOrderedSetAdvanceCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Copy mocks base method. +func (m *MockOrderedSet) Copy(syncScope bool) rangesync.OrderedSet { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Copy", syncScope) + ret0, _ := ret[0].(rangesync.OrderedSet) + return ret0 +} + +// Copy indicates an expected call of Copy. +func (mr *MockOrderedSetMockRecorder) Copy(syncScope any) *MockOrderedSetCopyCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockOrderedSet)(nil).Copy), syncScope) + return &MockOrderedSetCopyCall{Call: call} +} + +// MockOrderedSetCopyCall wrap *gomock.Call +type MockOrderedSetCopyCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetCopyCall) Return(arg0 rangesync.OrderedSet) *MockOrderedSetCopyCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetCopyCall) Do(f func(bool) rangesync.OrderedSet) *MockOrderedSetCopyCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetCopyCall) DoAndReturn(f func(bool) rangesync.OrderedSet) *MockOrderedSetCopyCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Empty mocks base method. +func (m *MockOrderedSet) Empty() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Empty") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Empty indicates an expected call of Empty. +func (mr *MockOrderedSetMockRecorder) Empty() *MockOrderedSetEmptyCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Empty", reflect.TypeOf((*MockOrderedSet)(nil).Empty)) + return &MockOrderedSetEmptyCall{Call: call} +} + +// MockOrderedSetEmptyCall wrap *gomock.Call +type MockOrderedSetEmptyCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetEmptyCall) Return(arg0 bool, arg1 error) *MockOrderedSetEmptyCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetEmptyCall) Do(f func() (bool, error)) *MockOrderedSetEmptyCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetEmptyCall) DoAndReturn(f func() (bool, error)) *MockOrderedSetEmptyCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// EnsureLoaded mocks base method. +func (m *MockOrderedSet) EnsureLoaded() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureLoaded") + ret0, _ := ret[0].(error) + return ret0 +} + +// EnsureLoaded indicates an expected call of EnsureLoaded. +func (mr *MockOrderedSetMockRecorder) EnsureLoaded() *MockOrderedSetEnsureLoadedCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureLoaded", reflect.TypeOf((*MockOrderedSet)(nil).EnsureLoaded)) + return &MockOrderedSetEnsureLoadedCall{Call: call} +} + +// MockOrderedSetEnsureLoadedCall wrap *gomock.Call +type MockOrderedSetEnsureLoadedCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetEnsureLoadedCall) Return(arg0 error) *MockOrderedSetEnsureLoadedCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetEnsureLoadedCall) Do(f func() error) *MockOrderedSetEnsureLoadedCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetEnsureLoadedCall) DoAndReturn(f func() error) *MockOrderedSetEnsureLoadedCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetRangeInfo mocks base method. +func (m *MockOrderedSet) GetRangeInfo(x, y rangesync.KeyBytes) (rangesync.RangeInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRangeInfo", x, y) + ret0, _ := ret[0].(rangesync.RangeInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRangeInfo indicates an expected call of GetRangeInfo. +func (mr *MockOrderedSetMockRecorder) GetRangeInfo(x, y any) *MockOrderedSetGetRangeInfoCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRangeInfo", reflect.TypeOf((*MockOrderedSet)(nil).GetRangeInfo), x, y) + return &MockOrderedSetGetRangeInfoCall{Call: call} +} + +// MockOrderedSetGetRangeInfoCall wrap *gomock.Call +type MockOrderedSetGetRangeInfoCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetGetRangeInfoCall) Return(arg0 rangesync.RangeInfo, arg1 error) *MockOrderedSetGetRangeInfoCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetGetRangeInfoCall) Do(f func(rangesync.KeyBytes, rangesync.KeyBytes) (rangesync.RangeInfo, error)) *MockOrderedSetGetRangeInfoCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetGetRangeInfoCall) DoAndReturn(f func(rangesync.KeyBytes, rangesync.KeyBytes) (rangesync.RangeInfo, error)) *MockOrderedSetGetRangeInfoCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Has mocks base method. +func (m *MockOrderedSet) Has(arg0 rangesync.KeyBytes) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Has", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Has indicates an expected call of Has. +func (mr *MockOrderedSetMockRecorder) Has(arg0 any) *MockOrderedSetHasCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Has", reflect.TypeOf((*MockOrderedSet)(nil).Has), arg0) + return &MockOrderedSetHasCall{Call: call} +} + +// MockOrderedSetHasCall wrap *gomock.Call +type MockOrderedSetHasCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetHasCall) Return(arg0 bool, arg1 error) *MockOrderedSetHasCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetHasCall) Do(f func(rangesync.KeyBytes) (bool, error)) *MockOrderedSetHasCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetHasCall) DoAndReturn(f func(rangesync.KeyBytes) (bool, error)) *MockOrderedSetHasCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Items mocks base method. +func (m *MockOrderedSet) Items() rangesync.SeqResult { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Items") + ret0, _ := ret[0].(rangesync.SeqResult) + return ret0 +} + +// Items indicates an expected call of Items. +func (mr *MockOrderedSetMockRecorder) Items() *MockOrderedSetItemsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Items", reflect.TypeOf((*MockOrderedSet)(nil).Items)) + return &MockOrderedSetItemsCall{Call: call} +} + +// MockOrderedSetItemsCall wrap *gomock.Call +type MockOrderedSetItemsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetItemsCall) Return(arg0 rangesync.SeqResult) *MockOrderedSetItemsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetItemsCall) Do(f func() rangesync.SeqResult) *MockOrderedSetItemsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetItemsCall) DoAndReturn(f func() rangesync.SeqResult) *MockOrderedSetItemsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Receive mocks base method. +func (m *MockOrderedSet) Receive(k rangesync.KeyBytes) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Receive", k) + ret0, _ := ret[0].(error) + return ret0 +} + +// Receive indicates an expected call of Receive. +func (mr *MockOrderedSetMockRecorder) Receive(k any) *MockOrderedSetReceiveCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Receive", reflect.TypeOf((*MockOrderedSet)(nil).Receive), k) + return &MockOrderedSetReceiveCall{Call: call} +} + +// MockOrderedSetReceiveCall wrap *gomock.Call +type MockOrderedSetReceiveCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetReceiveCall) Return(arg0 error) *MockOrderedSetReceiveCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetReceiveCall) Do(f func(rangesync.KeyBytes) error) *MockOrderedSetReceiveCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetReceiveCall) DoAndReturn(f func(rangesync.KeyBytes) error) *MockOrderedSetReceiveCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Received mocks base method. +func (m *MockOrderedSet) Received() rangesync.SeqResult { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Received") + ret0, _ := ret[0].(rangesync.SeqResult) + return ret0 +} + +// Received indicates an expected call of Received. +func (mr *MockOrderedSetMockRecorder) Received() *MockOrderedSetReceivedCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Received", reflect.TypeOf((*MockOrderedSet)(nil).Received)) + return &MockOrderedSetReceivedCall{Call: call} +} + +// MockOrderedSetReceivedCall wrap *gomock.Call +type MockOrderedSetReceivedCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetReceivedCall) Return(arg0 rangesync.SeqResult) *MockOrderedSetReceivedCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetReceivedCall) Do(f func() rangesync.SeqResult) *MockOrderedSetReceivedCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetReceivedCall) DoAndReturn(f func() rangesync.SeqResult) *MockOrderedSetReceivedCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Recent mocks base method. +func (m *MockOrderedSet) Recent(since time.Time) (rangesync.SeqResult, int) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Recent", since) + ret0, _ := ret[0].(rangesync.SeqResult) + ret1, _ := ret[1].(int) + return ret0, ret1 +} + +// Recent indicates an expected call of Recent. +func (mr *MockOrderedSetMockRecorder) Recent(since any) *MockOrderedSetRecentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recent", reflect.TypeOf((*MockOrderedSet)(nil).Recent), since) + return &MockOrderedSetRecentCall{Call: call} +} + +// MockOrderedSetRecentCall wrap *gomock.Call +type MockOrderedSetRecentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetRecentCall) Return(arg0 rangesync.SeqResult, arg1 int) *MockOrderedSetRecentCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetRecentCall) Do(f func(time.Time) (rangesync.SeqResult, int)) *MockOrderedSetRecentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetRecentCall) DoAndReturn(f func(time.Time) (rangesync.SeqResult, int)) *MockOrderedSetRecentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Release mocks base method. +func (m *MockOrderedSet) Release() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Release") + ret0, _ := ret[0].(error) + return ret0 +} + +// Release indicates an expected call of Release. +func (mr *MockOrderedSetMockRecorder) Release() *MockOrderedSetReleaseCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockOrderedSet)(nil).Release)) + return &MockOrderedSetReleaseCall{Call: call} +} + +// MockOrderedSetReleaseCall wrap *gomock.Call +type MockOrderedSetReleaseCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetReleaseCall) Return(arg0 error) *MockOrderedSetReleaseCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetReleaseCall) Do(f func() error) *MockOrderedSetReleaseCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetReleaseCall) DoAndReturn(f func() error) *MockOrderedSetReleaseCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SplitRange mocks base method. +func (m *MockOrderedSet) SplitRange(x, y rangesync.KeyBytes, count int) (rangesync.SplitInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SplitRange", x, y, count) + ret0, _ := ret[0].(rangesync.SplitInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SplitRange indicates an expected call of SplitRange. +func (mr *MockOrderedSetMockRecorder) SplitRange(x, y, count any) *MockOrderedSetSplitRangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SplitRange", reflect.TypeOf((*MockOrderedSet)(nil).SplitRange), x, y, count) + return &MockOrderedSetSplitRangeCall{Call: call} +} + +// MockOrderedSetSplitRangeCall wrap *gomock.Call +type MockOrderedSetSplitRangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetSplitRangeCall) Return(arg0 rangesync.SplitInfo, arg1 error) *MockOrderedSetSplitRangeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetSplitRangeCall) Do(f func(rangesync.KeyBytes, rangesync.KeyBytes, int) (rangesync.SplitInfo, error)) *MockOrderedSetSplitRangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetSplitRangeCall) DoAndReturn(f func(rangesync.KeyBytes, rangesync.KeyBytes, int) (rangesync.SplitInfo, error)) *MockOrderedSetSplitRangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockSyncBase is a mock of SyncBase interface. +type MockSyncBase struct { + ctrl *gomock.Controller + recorder *MockSyncBaseMockRecorder + isgomock struct{} +} + +// MockSyncBaseMockRecorder is the mock recorder for MockSyncBase. +type MockSyncBaseMockRecorder struct { + mock *MockSyncBase +} + +// NewMockSyncBase creates a new mock instance. +func NewMockSyncBase(ctrl *gomock.Controller) *MockSyncBase { + mock := &MockSyncBase{ctrl: ctrl} + mock.recorder = &MockSyncBaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSyncBase) EXPECT() *MockSyncBaseMockRecorder { + return m.recorder +} + +// Count mocks base method. +func (m *MockSyncBase) Count() (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Count") + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Count indicates an expected call of Count. +func (mr *MockSyncBaseMockRecorder) Count() *MockSyncBaseCountCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockSyncBase)(nil).Count)) + return &MockSyncBaseCountCall{Call: call} +} + +// MockSyncBaseCountCall wrap *gomock.Call +type MockSyncBaseCountCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncBaseCountCall) Return(arg0 int, arg1 error) *MockSyncBaseCountCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncBaseCountCall) Do(f func() (int, error)) *MockSyncBaseCountCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncBaseCountCall) DoAndReturn(f func() (int, error)) *MockSyncBaseCountCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Derive mocks base method. +func (m *MockSyncBase) Derive(p p2p.Peer) multipeer.Syncer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Derive", p) + ret0, _ := ret[0].(multipeer.Syncer) + return ret0 +} + +// Derive indicates an expected call of Derive. +func (mr *MockSyncBaseMockRecorder) Derive(p any) *MockSyncBaseDeriveCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Derive", reflect.TypeOf((*MockSyncBase)(nil).Derive), p) + return &MockSyncBaseDeriveCall{Call: call} +} + +// MockSyncBaseDeriveCall wrap *gomock.Call +type MockSyncBaseDeriveCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncBaseDeriveCall) Return(arg0 multipeer.Syncer) *MockSyncBaseDeriveCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncBaseDeriveCall) Do(f func(p2p.Peer) multipeer.Syncer) *MockSyncBaseDeriveCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncBaseDeriveCall) DoAndReturn(f func(p2p.Peer) multipeer.Syncer) *MockSyncBaseDeriveCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Probe mocks base method. +func (m *MockSyncBase) Probe(ctx context.Context, p p2p.Peer) (rangesync.ProbeResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Probe", ctx, p) + ret0, _ := ret[0].(rangesync.ProbeResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Probe indicates an expected call of Probe. +func (mr *MockSyncBaseMockRecorder) Probe(ctx, p any) *MockSyncBaseProbeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Probe", reflect.TypeOf((*MockSyncBase)(nil).Probe), ctx, p) + return &MockSyncBaseProbeCall{Call: call} +} + +// MockSyncBaseProbeCall wrap *gomock.Call +type MockSyncBaseProbeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncBaseProbeCall) Return(arg0 rangesync.ProbeResult, arg1 error) *MockSyncBaseProbeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncBaseProbeCall) Do(f func(context.Context, p2p.Peer) (rangesync.ProbeResult, error)) *MockSyncBaseProbeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncBaseProbeCall) DoAndReturn(f func(context.Context, p2p.Peer) (rangesync.ProbeResult, error)) *MockSyncBaseProbeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Wait mocks base method. +func (m *MockSyncBase) Wait() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Wait") + ret0, _ := ret[0].(error) + return ret0 +} + +// Wait indicates an expected call of Wait. +func (mr *MockSyncBaseMockRecorder) Wait() *MockSyncBaseWaitCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockSyncBase)(nil).Wait)) + return &MockSyncBaseWaitCall{Call: call} +} + +// MockSyncBaseWaitCall wrap *gomock.Call +type MockSyncBaseWaitCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncBaseWaitCall) Return(arg0 error) *MockSyncBaseWaitCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncBaseWaitCall) Do(f func() error) *MockSyncBaseWaitCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncBaseWaitCall) DoAndReturn(f func() error) *MockSyncBaseWaitCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockSyncer is a mock of Syncer interface. +type MockSyncer struct { + ctrl *gomock.Controller + recorder *MockSyncerMockRecorder + isgomock struct{} +} + +// MockSyncerMockRecorder is the mock recorder for MockSyncer. +type MockSyncerMockRecorder struct { + mock *MockSyncer +} + +// NewMockSyncer creates a new mock instance. +func NewMockSyncer(ctrl *gomock.Controller) *MockSyncer { + mock := &MockSyncer{ctrl: ctrl} + mock.recorder = &MockSyncerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSyncer) EXPECT() *MockSyncerMockRecorder { + return m.recorder +} + +// Peer mocks base method. +func (m *MockSyncer) Peer() p2p.Peer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Peer") + ret0, _ := ret[0].(p2p.Peer) + return ret0 +} + +// Peer indicates an expected call of Peer. +func (mr *MockSyncerMockRecorder) Peer() *MockSyncerPeerCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peer", reflect.TypeOf((*MockSyncer)(nil).Peer)) + return &MockSyncerPeerCall{Call: call} +} + +// MockSyncerPeerCall wrap *gomock.Call +type MockSyncerPeerCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncerPeerCall) Return(arg0 p2p.Peer) *MockSyncerPeerCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncerPeerCall) Do(f func() p2p.Peer) *MockSyncerPeerCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncerPeerCall) DoAndReturn(f func() p2p.Peer) *MockSyncerPeerCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Release mocks base method. +func (m *MockSyncer) Release() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Release") + ret0, _ := ret[0].(error) + return ret0 +} + +// Release indicates an expected call of Release. +func (mr *MockSyncerMockRecorder) Release() *MockSyncerReleaseCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockSyncer)(nil).Release)) + return &MockSyncerReleaseCall{Call: call} +} + +// MockSyncerReleaseCall wrap *gomock.Call +type MockSyncerReleaseCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncerReleaseCall) Return(arg0 error) *MockSyncerReleaseCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncerReleaseCall) Do(f func() error) *MockSyncerReleaseCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncerReleaseCall) DoAndReturn(f func() error) *MockSyncerReleaseCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Serve mocks base method. +func (m *MockSyncer) Serve(ctx context.Context, stream io.ReadWriter) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Serve", ctx, stream) + ret0, _ := ret[0].(error) + return ret0 +} + +// Serve indicates an expected call of Serve. +func (mr *MockSyncerMockRecorder) Serve(ctx, stream any) *MockSyncerServeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Serve", reflect.TypeOf((*MockSyncer)(nil).Serve), ctx, stream) + return &MockSyncerServeCall{Call: call} +} + +// MockSyncerServeCall wrap *gomock.Call +type MockSyncerServeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncerServeCall) Return(arg0 error) *MockSyncerServeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncerServeCall) Do(f func(context.Context, io.ReadWriter) error) *MockSyncerServeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncerServeCall) DoAndReturn(f func(context.Context, io.ReadWriter) error) *MockSyncerServeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Sync mocks base method. +func (m *MockSyncer) Sync(ctx context.Context, x, y rangesync.KeyBytes) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Sync", ctx, x, y) + ret0, _ := ret[0].(error) + return ret0 +} + +// Sync indicates an expected call of Sync. +func (mr *MockSyncerMockRecorder) Sync(ctx, x, y any) *MockSyncerSyncCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sync", reflect.TypeOf((*MockSyncer)(nil).Sync), ctx, x, y) + return &MockSyncerSyncCall{Call: call} +} + +// MockSyncerSyncCall wrap *gomock.Call +type MockSyncerSyncCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncerSyncCall) Return(arg0 error) *MockSyncerSyncCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncerSyncCall) Do(f func(context.Context, rangesync.KeyBytes, rangesync.KeyBytes) error) *MockSyncerSyncCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncerSyncCall) DoAndReturn(f func(context.Context, rangesync.KeyBytes, rangesync.KeyBytes) error) *MockSyncerSyncCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockSyncKeyHandler is a mock of SyncKeyHandler interface. +type MockSyncKeyHandler struct { + ctrl *gomock.Controller + recorder *MockSyncKeyHandlerMockRecorder + isgomock struct{} +} + +// MockSyncKeyHandlerMockRecorder is the mock recorder for MockSyncKeyHandler. +type MockSyncKeyHandlerMockRecorder struct { + mock *MockSyncKeyHandler +} + +// NewMockSyncKeyHandler creates a new mock instance. +func NewMockSyncKeyHandler(ctrl *gomock.Controller) *MockSyncKeyHandler { + mock := &MockSyncKeyHandler{ctrl: ctrl} + mock.recorder = &MockSyncKeyHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSyncKeyHandler) EXPECT() *MockSyncKeyHandlerMockRecorder { + return m.recorder +} + +// Commit mocks base method. +func (m *MockSyncKeyHandler) Commit(peer p2p.Peer, base, new multipeer.OrderedSet) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", peer, base, new) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *MockSyncKeyHandlerMockRecorder) Commit(peer, base, new any) *MockSyncKeyHandlerCommitCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockSyncKeyHandler)(nil).Commit), peer, base, new) + return &MockSyncKeyHandlerCommitCall{Call: call} +} + +// MockSyncKeyHandlerCommitCall wrap *gomock.Call +type MockSyncKeyHandlerCommitCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncKeyHandlerCommitCall) Return(arg0 error) *MockSyncKeyHandlerCommitCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncKeyHandlerCommitCall) Do(f func(p2p.Peer, multipeer.OrderedSet, multipeer.OrderedSet) error) *MockSyncKeyHandlerCommitCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncKeyHandlerCommitCall) DoAndReturn(f func(p2p.Peer, multipeer.OrderedSet, multipeer.OrderedSet) error) *MockSyncKeyHandlerCommitCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Receive mocks base method. +func (m *MockSyncKeyHandler) Receive(k rangesync.KeyBytes, peer p2p.Peer) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Receive", k, peer) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Receive indicates an expected call of Receive. +func (mr *MockSyncKeyHandlerMockRecorder) Receive(k, peer any) *MockSyncKeyHandlerReceiveCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Receive", reflect.TypeOf((*MockSyncKeyHandler)(nil).Receive), k, peer) + return &MockSyncKeyHandlerReceiveCall{Call: call} +} + +// MockSyncKeyHandlerReceiveCall wrap *gomock.Call +type MockSyncKeyHandlerReceiveCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSyncKeyHandlerReceiveCall) Return(arg0 bool, arg1 error) *MockSyncKeyHandlerReceiveCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSyncKeyHandlerReceiveCall) Do(f func(rangesync.KeyBytes, p2p.Peer) (bool, error)) *MockSyncKeyHandlerReceiveCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSyncKeyHandlerReceiveCall) DoAndReturn(f func(rangesync.KeyBytes, p2p.Peer) (bool, error)) *MockSyncKeyHandlerReceiveCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockPairwiseSyncer is a mock of PairwiseSyncer interface. +type MockPairwiseSyncer struct { + ctrl *gomock.Controller + recorder *MockPairwiseSyncerMockRecorder + isgomock struct{} +} + +// MockPairwiseSyncerMockRecorder is the mock recorder for MockPairwiseSyncer. +type MockPairwiseSyncerMockRecorder struct { + mock *MockPairwiseSyncer +} + +// NewMockPairwiseSyncer creates a new mock instance. +func NewMockPairwiseSyncer(ctrl *gomock.Controller) *MockPairwiseSyncer { + mock := &MockPairwiseSyncer{ctrl: ctrl} + mock.recorder = &MockPairwiseSyncerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPairwiseSyncer) EXPECT() *MockPairwiseSyncerMockRecorder { + return m.recorder +} + +// Probe mocks base method. +func (m *MockPairwiseSyncer) Probe(ctx context.Context, peer p2p.Peer, os rangesync.OrderedSet, x, y rangesync.KeyBytes) (rangesync.ProbeResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Probe", ctx, peer, os, x, y) + ret0, _ := ret[0].(rangesync.ProbeResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Probe indicates an expected call of Probe. +func (mr *MockPairwiseSyncerMockRecorder) Probe(ctx, peer, os, x, y any) *MockPairwiseSyncerProbeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Probe", reflect.TypeOf((*MockPairwiseSyncer)(nil).Probe), ctx, peer, os, x, y) + return &MockPairwiseSyncerProbeCall{Call: call} +} + +// MockPairwiseSyncerProbeCall wrap *gomock.Call +type MockPairwiseSyncerProbeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPairwiseSyncerProbeCall) Return(arg0 rangesync.ProbeResult, arg1 error) *MockPairwiseSyncerProbeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPairwiseSyncerProbeCall) Do(f func(context.Context, p2p.Peer, rangesync.OrderedSet, rangesync.KeyBytes, rangesync.KeyBytes) (rangesync.ProbeResult, error)) *MockPairwiseSyncerProbeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPairwiseSyncerProbeCall) DoAndReturn(f func(context.Context, p2p.Peer, rangesync.OrderedSet, rangesync.KeyBytes, rangesync.KeyBytes) (rangesync.ProbeResult, error)) *MockPairwiseSyncerProbeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Serve mocks base method. +func (m *MockPairwiseSyncer) Serve(context context.Context, stream io.ReadWriter, os rangesync.OrderedSet) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Serve", context, stream, os) + ret0, _ := ret[0].(error) + return ret0 +} + +// Serve indicates an expected call of Serve. +func (mr *MockPairwiseSyncerMockRecorder) Serve(context, stream, os any) *MockPairwiseSyncerServeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Serve", reflect.TypeOf((*MockPairwiseSyncer)(nil).Serve), context, stream, os) + return &MockPairwiseSyncerServeCall{Call: call} +} + +// MockPairwiseSyncerServeCall wrap *gomock.Call +type MockPairwiseSyncerServeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPairwiseSyncerServeCall) Return(arg0 error) *MockPairwiseSyncerServeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPairwiseSyncerServeCall) Do(f func(context.Context, io.ReadWriter, rangesync.OrderedSet) error) *MockPairwiseSyncerServeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPairwiseSyncerServeCall) DoAndReturn(f func(context.Context, io.ReadWriter, rangesync.OrderedSet) error) *MockPairwiseSyncerServeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Sync mocks base method. +func (m *MockPairwiseSyncer) Sync(ctx context.Context, peer p2p.Peer, os rangesync.OrderedSet, x, y rangesync.KeyBytes) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Sync", ctx, peer, os, x, y) + ret0, _ := ret[0].(error) + return ret0 +} + +// Sync indicates an expected call of Sync. +func (mr *MockPairwiseSyncerMockRecorder) Sync(ctx, peer, os, x, y any) *MockPairwiseSyncerSyncCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sync", reflect.TypeOf((*MockPairwiseSyncer)(nil).Sync), ctx, peer, os, x, y) + return &MockPairwiseSyncerSyncCall{Call: call} +} + +// MockPairwiseSyncerSyncCall wrap *gomock.Call +type MockPairwiseSyncerSyncCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockPairwiseSyncerSyncCall) Return(arg0 error) *MockPairwiseSyncerSyncCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockPairwiseSyncerSyncCall) Do(f func(context.Context, p2p.Peer, rangesync.OrderedSet, rangesync.KeyBytes, rangesync.KeyBytes) error) *MockPairwiseSyncerSyncCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockPairwiseSyncerSyncCall) DoAndReturn(f func(context.Context, p2p.Peer, rangesync.OrderedSet, rangesync.KeyBytes, rangesync.KeyBytes) error) *MockPairwiseSyncerSyncCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MocksyncRunner is a mock of syncRunner interface. +type MocksyncRunner struct { + ctrl *gomock.Controller + recorder *MocksyncRunnerMockRecorder + isgomock struct{} +} + +// MocksyncRunnerMockRecorder is the mock recorder for MocksyncRunner. +type MocksyncRunnerMockRecorder struct { + mock *MocksyncRunner +} + +// NewMocksyncRunner creates a new mock instance. +func NewMocksyncRunner(ctrl *gomock.Controller) *MocksyncRunner { + mock := &MocksyncRunner{ctrl: ctrl} + mock.recorder = &MocksyncRunnerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MocksyncRunner) EXPECT() *MocksyncRunnerMockRecorder { + return m.recorder +} + +// FullSync mocks base method. +func (m *MocksyncRunner) FullSync(ctx context.Context, syncPeers []p2p.Peer) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FullSync", ctx, syncPeers) + ret0, _ := ret[0].(error) + return ret0 +} + +// FullSync indicates an expected call of FullSync. +func (mr *MocksyncRunnerMockRecorder) FullSync(ctx, syncPeers any) *MocksyncRunnerFullSyncCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FullSync", reflect.TypeOf((*MocksyncRunner)(nil).FullSync), ctx, syncPeers) + return &MocksyncRunnerFullSyncCall{Call: call} +} + +// MocksyncRunnerFullSyncCall wrap *gomock.Call +type MocksyncRunnerFullSyncCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MocksyncRunnerFullSyncCall) Return(arg0 error) *MocksyncRunnerFullSyncCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MocksyncRunnerFullSyncCall) Do(f func(context.Context, []p2p.Peer) error) *MocksyncRunnerFullSyncCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MocksyncRunnerFullSyncCall) DoAndReturn(f func(context.Context, []p2p.Peer) error) *MocksyncRunnerFullSyncCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SplitSync mocks base method. +func (m *MocksyncRunner) SplitSync(ctx context.Context, syncPeers []p2p.Peer) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SplitSync", ctx, syncPeers) + ret0, _ := ret[0].(error) + return ret0 +} + +// SplitSync indicates an expected call of SplitSync. +func (mr *MocksyncRunnerMockRecorder) SplitSync(ctx, syncPeers any) *MocksyncRunnerSplitSyncCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SplitSync", reflect.TypeOf((*MocksyncRunner)(nil).SplitSync), ctx, syncPeers) + return &MocksyncRunnerSplitSyncCall{Call: call} +} + +// MocksyncRunnerSplitSyncCall wrap *gomock.Call +type MocksyncRunnerSplitSyncCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MocksyncRunnerSplitSyncCall) Return(arg0 error) *MocksyncRunnerSplitSyncCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MocksyncRunnerSplitSyncCall) Do(f func(context.Context, []p2p.Peer) error) *MocksyncRunnerSplitSyncCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MocksyncRunnerSplitSyncCall) DoAndReturn(f func(context.Context, []p2p.Peer) error) *MocksyncRunnerSplitSyncCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/sync2/multipeer/multipeer.go b/sync2/multipeer/multipeer.go new file mode 100644 index 0000000000..67ad2c9925 --- /dev/null +++ b/sync2/multipeer/multipeer.go @@ -0,0 +1,463 @@ +package multipeer + +import ( + "context" + "errors" + "math" + "time" + + "github.com/jonboulle/clockwork" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/fetch/peers" + "github.com/spacemeshos/go-spacemesh/p2p" +) + +type syncability struct { + // peers that were probed successfully + syncable []p2p.Peer + // peers that have enough items for split sync + splitSyncable []p2p.Peer + // Number of peers that are similar enough to this one for full sync + nearFullCount int +} + +// MultiPeerReconcilerOpt specifies an option for a MultiPeerReconciler. +type MultiPeerReconcilerOpt func(mpr *MultiPeerReconciler) + +// WithSyncPeerCount sets the number of peers to pick for synchronization. +// Synchronization will still happen if fewer peers are available. +func WithSyncPeerCount(count int) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.syncPeerCount = count + } +} + +// WithMinSplitSyncCount sets the minimum number of items that a peer must have to be +// eligible for split sync (subrange-per-peer). +func WithMinSplitSyncCount(count int) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.minSplitSyncCount = count + } +} + +// WithMaxFullDiff specifies the maximum approximate size of symmetric difference between +// the local set and the remote one for the sets to be considered "mostly in sync", so +// that full sync is preferred to split sync. +func WithMaxFullDiff(diff int) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.maxFullDiff = diff + } +} + +// WithMaxSyncDiff specifies the maximum number of items that a peer can have less than the +// local set for it to be considered for synchronization. +func WithMaxSyncDiff(diff int) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.maxSyncDiff = diff + } +} + +// WithSyncInterval specifies the interval between syncs. +func WithSyncInterval(d time.Duration) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.syncInterval = d + } +} + +// WithRetryInterval specifies the interval between retries after a failed sync. +func WithRetryInterval(d time.Duration) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.retryInterval = d + } +} + +// WithNoPeersRecheckInterval specifies the interval between rechecking for peers after no +// synchronization peers were found. +func WithNoPeersRecheckInterval(d time.Duration) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.noPeersRecheckInterval = d + } +} + +// WithMinSplitSyncPeers specifies the minimum number of peers for the split +// sync to happen. +func WithMinSplitSyncPeers(n int) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.minSplitSyncPeers = n + } +} + +// WithMinCompleteFraction specifies the minimum fraction (0..1) of "mostly synced" peers +// starting with which full sync is used instead of split sync. +func WithMinCompleteFraction(f float64) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.minCompleteFraction = f + } +} + +// WithSplitSyncGracePeriod specifies grace period for split sync peers. +// If a peer doesn't complete syncing its range within the specified duration during split +// sync, its range is assigned additionally to another quicker peer. The sync against the +// "slow" peer is NOT stopped immediately after that. +func WithSplitSyncGracePeriod(t time.Duration) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.splitSyncGracePeriod = t + } +} + +// WithLogger specifies the logger for the MultiPeerReconciler. +func WithLogger(logger *zap.Logger) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.logger = logger + } +} + +// WithMinFullSyncednessCount sets the minimum number of full syncs that must +// have happened within the fullSyncednessPeriod for the node to be considered +// fully synced. +func WithMinFullSyncednessCount(count int) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.minFullSyncednessCount = count + } +} + +// WithFullSyncednessPeriod sets the duration within which the minimum number +// of full syncs must have happened for the node to be considered fully synced. +func WithFullSyncednessPeriod(d time.Duration) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.fullSyncednessPeriod = d + } +} + +func withClock(clock clockwork.Clock) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.clock = clock + } +} + +func withSyncRunner(runner syncRunner) MultiPeerReconcilerOpt { + return func(mpr *MultiPeerReconciler) { + mpr.runner = runner + } +} + +type runner struct { + mpr *MultiPeerReconciler +} + +var _ syncRunner = &runner{} + +func (r *runner) SplitSync(ctx context.Context, syncPeers []p2p.Peer) error { + s := newSplitSync( + r.mpr.logger, r.mpr.syncBase, r.mpr.peers, syncPeers, + r.mpr.splitSyncGracePeriod, r.mpr.clock, r.mpr.keyLen, r.mpr.maxDepth) + return s.Sync(ctx) +} + +func (r *runner) FullSync(ctx context.Context, syncPeers []p2p.Peer) error { + return r.mpr.fullSync(ctx, syncPeers) +} + +// MultiPeerReconciler reconcilies the local set against multiple remote sets. +type MultiPeerReconciler struct { + logger *zap.Logger + syncBase SyncBase + peers *peers.Peers + syncPeerCount int + minSplitSyncPeers int + minSplitSyncCount int + maxFullDiff int + maxSyncDiff int + minCompleteFraction float64 + splitSyncGracePeriod time.Duration + syncInterval time.Duration + retryInterval time.Duration + noPeersRecheckInterval time.Duration + clock clockwork.Clock + keyLen int + maxDepth int + runner syncRunner + minFullSyncednessCount int + fullSyncednessPeriod time.Duration + sl *syncList +} + +// NewMultiPeerReconciler creates a new MultiPeerReconciler. +func NewMultiPeerReconciler( + syncBase SyncBase, + peers *peers.Peers, + keyLen, maxDepth int, + opts ...MultiPeerReconcilerOpt, +) *MultiPeerReconciler { + mpr := &MultiPeerReconciler{ + logger: zap.NewNop(), + syncBase: syncBase, + peers: peers, + syncPeerCount: 20, + minSplitSyncPeers: 2, + minSplitSyncCount: 1000, + maxFullDiff: 10000, + maxSyncDiff: 100, + syncInterval: 5 * time.Minute, + retryInterval: 1 * time.Minute, + minCompleteFraction: 0.5, + splitSyncGracePeriod: time.Minute, + noPeersRecheckInterval: 30 * time.Second, + clock: clockwork.NewRealClock(), + keyLen: keyLen, + maxDepth: maxDepth, + minFullSyncednessCount: 1, // TODO: use at least 3 and make it configurable + fullSyncednessPeriod: 15 * time.Minute, + } + for _, opt := range opts { + opt(mpr) + } + if mpr.runner == nil { + mpr.runner = &runner{mpr: mpr} + } + mpr.sl = newSyncList(mpr.clock, mpr.minFullSyncednessCount, mpr.fullSyncednessPeriod) + return mpr +} + +func (mpr *MultiPeerReconciler) probePeers(ctx context.Context, syncPeers []p2p.Peer) (syncability, error) { + var s syncability + s.syncable = nil + s.splitSyncable = nil + s.nearFullCount = 0 + for _, p := range syncPeers { + mpr.logger.Debug("probe peer", zap.Stringer("peer", p)) + pr, err := mpr.syncBase.Probe(ctx, p) + if err != nil { + mpr.logger.Warn("error probing the peer", zap.Any("peer", p), zap.Error(err)) + if errors.Is(err, context.Canceled) { + return s, err + } + continue + } + + c, err := mpr.syncBase.Count() + if err != nil { + return s, err + } + + // We do not consider peers with substantially fewer items than the local + // set for active sync. It's these peers' responsibility to request sync + // against this node. + if pr.Count+mpr.maxSyncDiff < c { + mpr.logger.Debug("skipping peer with low item count", + zap.Int("peerCount", pr.Count), + zap.Int("localCount", c)) + continue + } + + s.syncable = append(s.syncable, p) + if pr.Count > mpr.minSplitSyncCount { + mpr.logger.Debug("splitSyncable peer", + zap.Stringer("peer", p), + zap.Int("count", pr.Count)) + s.splitSyncable = append(s.splitSyncable, p) + } else { + mpr.logger.Debug("NOT splitSyncable peer", + zap.Stringer("peer", p), + zap.Int("count", pr.Count)) + } + + mDiff := float64(mpr.maxFullDiff) + if math.Abs(float64(pr.Count-c)) < mDiff && (1-pr.Sim)*float64(c) < mDiff { + mpr.logger.Debug("nearFull peer", + zap.Stringer("peer", p), + zap.Float64("sim", pr.Sim), + zap.Int("localCount", c)) + s.nearFullCount++ + } else { + mpr.logger.Debug("nearFull peer", + zap.Stringer("peer", p), + zap.Float64("sim", pr.Sim), + zap.Int("localCount", c)) + } + } + return s, nil +} + +func (mpr *MultiPeerReconciler) needSplitSync(s syncability) bool { + if float64(s.nearFullCount) >= float64(len(s.syncable))*mpr.minCompleteFraction { + // enough peers are close to this one according to minhash score, can do + // full sync + mpr.logger.Debug("enough peers are close to this one, doing full sync", + zap.Int("nearFullCount", s.nearFullCount), + zap.Int("peerCount", len(s.syncable)), + zap.Float64("minCompleteFraction", mpr.minCompleteFraction)) + return false + } + + if len(s.splitSyncable) < mpr.minSplitSyncPeers { + // would be nice to do split sync, but not enough peers for that + mpr.logger.Debug("not enough peers for split sync", + zap.Int("splitSyncableCount", len(s.splitSyncable)), + zap.Int("minSplitSyncPeers", mpr.minSplitSyncPeers)) + return false + } + + mpr.logger.Debug("can do split sync") + return true +} + +func (mpr *MultiPeerReconciler) fullSync(ctx context.Context, syncPeers []p2p.Peer) error { + var eg errgroup.Group + for _, p := range syncPeers { + syncer := mpr.syncBase.Derive(p) + eg.Go(func() error { + defer syncer.Release() + err := syncer.Sync(ctx, nil, nil) + switch { + case err == nil: + mpr.sl.NoteSync() + case errors.Is(err, context.Canceled): + return err + default: + // failing to sync against a particular peer is not considered + // a fatal sync failure, so we just log the error + mpr.logger.Error("error syncing peer", zap.Stringer("peer", p), zap.Error(err)) + } + return syncer.Release() + }) + } + return eg.Wait() +} + +func (mpr *MultiPeerReconciler) syncOnce(ctx context.Context, lastWasSplit bool) (full bool, err error) { + var s syncability + for { + syncPeers := mpr.peers.SelectBest(mpr.syncPeerCount) + mpr.logger.Debug("selected best peers for sync", + zap.Int("syncPeerCount", mpr.syncPeerCount), + zap.Int("totalPeers", mpr.peers.Total()), + zap.Int("numSelected", len(syncPeers))) + if len(syncPeers) != 0 { + // probePeers doesn't return transient errors, sync must stop if it failed + mpr.logger.Debug("probing peers", zap.Int("count", len(syncPeers))) + s, err = mpr.probePeers(ctx, syncPeers) + if err != nil { + return false, err + } + if len(s.syncable) != 0 { + break + } + } + + mpr.logger.Debug("no peers found, waiting", zap.Duration("duration", mpr.noPeersRecheckInterval)) + select { + case <-ctx.Done(): + return false, ctx.Err() + case <-mpr.clock.After(mpr.noPeersRecheckInterval): + } + } + + full = false + if !lastWasSplit && mpr.needSplitSync(s) { + mpr.logger.Debug("doing split sync", zap.Int("peerCount", len(s.splitSyncable))) + err = mpr.runner.SplitSync(ctx, s.splitSyncable) + if err != nil { + mpr.logger.Debug("split sync failed", zap.Error(err)) + } else { + mpr.logger.Debug("split sync complete") + } + } else { + full = true + mpr.logger.Debug("doing full sync", zap.Int("peerCount", len(s.syncable))) + err = mpr.runner.FullSync(ctx, s.syncable) + if err != nil { + mpr.logger.Debug("full sync failed", zap.Error(err)) + } else { + mpr.logger.Debug("full sync complete") + } + } + + // handler errors are not fatal + if handlerErr := mpr.syncBase.Wait(); handlerErr != nil { + mpr.logger.Error("error handling synced keys", zap.Error(handlerErr)) + } + + return full, err +} + +// Run runs the MultiPeerReconciler. +func (mpr *MultiPeerReconciler) Run(ctx context.Context) error { + // The point of using split sync, which syncs different key ranges against + // different peers, vs full sync which syncs the full key range against different + // peers, is: + // 1. Avoid getting too many range splits and thus network transfer overhead + // 2. Avoid fetching same keys from multiple peers + + // States: + // A. Wait. Pause for sync interval + // Timeout => A + // B. No peers -> do nothing. + // Got any peers => C + // C. Low on peers. Wait for more to appear + // Lost all peers => B + // Got enough peers => D + // Timeout => D + // D. Probe the peers. Use successfully probed ones in states E/F + // Drop failed peers from the peer set while polling. + // All probes failed => B + // Last sync was split sync => E + // N of peers < minSplitSyncPeers => E + // All are low on count (minSplitSyncCount) => F + // Enough peers (minCompleteFraction) with diffSize <= maxFullDiff => E + // diffSize = (1-sim)*localItemCount + // Otherwise => F + // E. Full sync. Run full syncs against each peer + // All syncs completed (success / fail) => A + // F. Bounded sync. Subdivide the range by peers and start syncs. + // Use peers with > minSplitSyncCount + // Wait for all the syncs to complete/fail + // All syncs completed (success / fail) => A + ctx, cancel := context.WithCancel(ctx) + var ( + err error + full bool + ) + lastWasSplit := false +LOOP: + for { + interval := mpr.syncInterval + full, err = mpr.syncOnce(ctx, lastWasSplit) + if err != nil { + if errors.Is(err, context.Canceled) { + break + } + mpr.logger.Error("sync failed", zap.Bool("full", full), zap.Error(err)) + interval = mpr.retryInterval + } else if !full { + // Split sync needs to be followed by a full sync. + // Don't wait to have sync move forward quicker. + // In most cases, the full sync will be very quick. + lastWasSplit = true + mpr.logger.Debug("redo sync after split sync") + continue + } + lastWasSplit = false + mpr.logger.Debug("pausing sync", zap.Duration("interval", interval)) + select { + case <-ctx.Done(): + err = ctx.Err() + break LOOP + case <-mpr.clock.After(interval): + } + } + cancel() + if err != nil { + mpr.syncBase.Wait() + return err + } + return mpr.syncBase.Wait() +} + +// Synced returns true if the node is considered synced, that is, the specified +// number of full syncs has happened within the specified duration of time. +func (mpr *MultiPeerReconciler) Synced() bool { + return mpr.sl.Synced() +} diff --git a/sync2/multipeer/multipeer_test.go b/sync2/multipeer/multipeer_test.go new file mode 100644 index 0000000000..4a178a501a --- /dev/null +++ b/sync2/multipeer/multipeer_test.go @@ -0,0 +1,376 @@ +package multipeer_test + +import ( + "context" + "errors" + "fmt" + "slices" + "sync" + "testing" + "time" + + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/fetch/peers" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/sync2/multipeer" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +// FIXME: BlockUntilContext is not included in FakeClock interface. +// This will be fixed in a post-0.4.0 clockwork release, but with a breaking change that +// makes FakeClock a struct instead of an interface. +// See: https://github.com/jonboulle/clockwork/pull/71 +type fakeClock interface { + clockwork.FakeClock + BlockUntilContext(ctx context.Context, n int) error +} + +type peerList struct { + sync.Mutex + peers []p2p.Peer +} + +func (pl *peerList) add(p p2p.Peer) bool { + pl.Lock() + defer pl.Unlock() + if slices.Contains(pl.peers, p) { + return false + } + pl.peers = append(pl.peers, p) + return true +} + +func (pl *peerList) get() []p2p.Peer { + pl.Lock() + defer pl.Unlock() + return slices.Clone(pl.peers) +} + +type multiPeerSyncTester struct { + *testing.T + ctrl *gomock.Controller + syncBase *MockSyncBase + syncRunner *MocksyncRunner + peers *peers.Peers + clock fakeClock + reconciler *multipeer.MultiPeerReconciler + cancel context.CancelFunc + eg errgroup.Group + // EXPECT() calls should not be done concurrently + // https://github.com/golang/mock/issues/533#issuecomment-821537840 + mtx sync.Mutex +} + +func newMultiPeerSyncTester(t *testing.T) *multiPeerSyncTester { + ctrl := gomock.NewController(t) + mt := &multiPeerSyncTester{ + T: t, + ctrl: ctrl, + syncBase: NewMockSyncBase(ctrl), + syncRunner: NewMocksyncRunner(ctrl), + peers: peers.New(), + clock: clockwork.NewFakeClock().(fakeClock), + } + mt.reconciler = multipeer.NewMultiPeerReconciler(mt.syncBase, mt.peers, 32, 24, + multipeer.WithLogger(zaptest.NewLogger(t)), + multipeer.WithSyncInterval(time.Minute), + multipeer.WithSyncPeerCount(6), + multipeer.WithMinSplitSyncPeers(2), + multipeer.WithMinSplitSyncCount(90), + multipeer.WithMaxFullDiff(20), + multipeer.WithMinCompleteFraction(0.9), + multipeer.WithNoPeersRecheckInterval(10*time.Second), + multipeer.WithSyncRunner(mt.syncRunner), + multipeer.WithClock(mt.clock)) + return mt +} + +func (mt *multiPeerSyncTester) addPeers(n int) []p2p.Peer { + r := make([]p2p.Peer, n) + for i := 0; i < n; i++ { + p := p2p.Peer(fmt.Sprintf("peer%d", i+1)) + mt.peers.Add(p) + r[i] = p + } + return r +} + +func (mt *multiPeerSyncTester) start() context.Context { + var ctx context.Context + ctx, mt.cancel = context.WithTimeout(context.Background(), 10*time.Second) + mt.eg.Go(func() error { return mt.reconciler.Run(ctx) }) + mt.Cleanup(func() { + mt.cancel() + if err := mt.eg.Wait(); err != nil { + require.ErrorIs(mt, err, context.Canceled) + } + }) + return ctx +} + +func (mt *multiPeerSyncTester) expectProbe(times int, pr rangesync.ProbeResult) *peerList { + var pl peerList + mt.syncBase.EXPECT().Probe(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, p p2p.Peer) (rangesync.ProbeResult, error) { + require.True(mt, pl.add(p), "peer shouldn't be probed twice") + require.True(mt, mt.peers.Contains(p)) + return pr, nil + }).Times(times) + return &pl +} + +func (mt *multiPeerSyncTester) expectSingleProbe( + peer p2p.Peer, + pr rangesync.ProbeResult, +) { + mt.syncBase.EXPECT().Probe(gomock.Any(), peer).DoAndReturn( + func(_ context.Context, p p2p.Peer) (rangesync.ProbeResult, error) { + return pr, nil + }) +} + +func (mt *multiPeerSyncTester) expectFullSync(pl *peerList, times, numFails int) { + mt.syncRunner.EXPECT().FullSync(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, peers []p2p.Peer) error { + require.ElementsMatch(mt, pl.get(), peers) + // delegate to the real fullsync + return mt.reconciler.FullSync(ctx, peers) + }) + mt.syncBase.EXPECT().Derive(gomock.Any()).DoAndReturn(func(p p2p.Peer) multipeer.Syncer { + mt.mtx.Lock() + defer mt.mtx.Unlock() + require.Contains(mt, pl.get(), p) + s := NewMockSyncer(mt.ctrl) + s.EXPECT().Peer().Return(p).AnyTimes() + // TODO: do better job at tracking Release() calls + s.EXPECT().Release().AnyTimes() + expSync := s.EXPECT().Sync(gomock.Any(), gomock.Nil(), gomock.Nil()) + if numFails != 0 { + expSync.Return(errors.New("sync failed")) + numFails-- + } + return s + }).Times(times) +} + +// satisfy waits until all the expected mocked calls are made. +func (mt *multiPeerSyncTester) satisfy() { + require.Eventually(mt, func() bool { + mt.mtx.Lock() + defer mt.mtx.Unlock() + return mt.ctrl.Satisfied() + }, time.Second, time.Millisecond) +} + +func TestMultiPeerSync(t *testing.T) { + const numSyncs = 3 + + t.Run("split sync", func(t *testing.T) { + mt := newMultiPeerSyncTester(t) + ctx := mt.start() + mt.clock.BlockUntilContext(ctx, 1) + // Advance by sync interval. No peers yet + mt.clock.Advance(time.Minute) + mt.clock.BlockUntilContext(ctx, 1) + // It is safe to do EXPECT() calls while the MultiPeerReconciler is blocked + mt.addPeers(10) + // Advance by peer wait time. After that, 6 peers will be selected + // randomly and probed + mt.syncBase.EXPECT().Count().Return(50, nil).AnyTimes() + for i := 0; i < numSyncs; i++ { + plSplit := mt.expectProbe(6, rangesync.ProbeResult{ + FP: "foo", + Count: 100, + Sim: 0.5, // too low for full sync + }) + mt.syncRunner.EXPECT().SplitSync(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, peers []p2p.Peer) error { + require.ElementsMatch(t, plSplit.get(), peers) + return nil + }) + mt.syncBase.EXPECT().Wait() + mt.clock.BlockUntilContext(ctx, 1) + plFull := mt.expectProbe(6, rangesync.ProbeResult{ + FP: "foo", + Count: 100, + Sim: 1, // after sync + }) + mt.expectFullSync(plFull, 6, 0) + mt.syncBase.EXPECT().Wait() + if i > 0 { + mt.clock.Advance(time.Minute) + } else if i < numSyncs-1 { + mt.clock.Advance(10 * time.Second) + } + mt.satisfy() + } + mt.syncBase.EXPECT().Wait() + }) + + t.Run("full sync", func(t *testing.T) { + mt := newMultiPeerSyncTester(t) + mt.addPeers(10) + mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() + require.False(t, mt.reconciler.Synced()) + var ctx context.Context + for i := 0; i < numSyncs; i++ { + pl := mt.expectProbe(6, rangesync.ProbeResult{ + FP: "foo", + Count: 100, + Sim: 0.99, // high enough for full sync + }) + mt.expectFullSync(pl, 6, 0) + mt.syncBase.EXPECT().Wait() + if i == 0 { + //nolint:fatcontext + ctx = mt.start() + } else { + // first full sync happens immediately + mt.clock.Advance(time.Minute) + } + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + } + require.True(t, mt.reconciler.Synced()) + mt.syncBase.EXPECT().Wait() + }) + + t.Run("full sync, peers with low count ignored", func(t *testing.T) { + mt := newMultiPeerSyncTester(t) + addedPeers := mt.addPeers(6) + mt.syncBase.EXPECT().Count().Return(1000, nil).AnyTimes() + require.False(t, mt.reconciler.Synced()) + var ctx context.Context + for i := 0; i < numSyncs; i++ { + var pl peerList + for _, p := range addedPeers[:5] { + mt.expectSingleProbe(p, rangesync.ProbeResult{ + FP: "foo", + Count: 1000, + Sim: 0.99, // high enough for full sync + }) + pl.add(p) + } + mt.expectSingleProbe(addedPeers[5], rangesync.ProbeResult{ + FP: "foo", + Count: 800, // count too low, this peer should be ignored + Sim: 0.9, + }) + mt.expectFullSync(&pl, 5, 0) + mt.syncBase.EXPECT().Wait() + if i == 0 { + //nolint:fatcontext + ctx = mt.start() + } else { + // first full sync happens immediately + mt.clock.Advance(time.Minute) + } + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + } + require.True(t, mt.reconciler.Synced()) + mt.syncBase.EXPECT().Wait() + }) + + t.Run("full sync due to low peer count", func(t *testing.T) { + mt := newMultiPeerSyncTester(t) + mt.addPeers(1) + mt.syncBase.EXPECT().Count().Return(50, nil).AnyTimes() + var ctx context.Context + for i := 0; i < numSyncs; i++ { + pl := mt.expectProbe(1, rangesync.ProbeResult{ + FP: "foo", + Count: 100, + Sim: 0.5, // too low for full sync, but will have it anyway + }) + mt.expectFullSync(pl, 1, 0) + mt.syncBase.EXPECT().Wait() + if i == 0 { + //nolint:fatcontext + ctx = mt.start() + } else { + mt.clock.Advance(time.Minute) + } + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + } + mt.syncBase.EXPECT().Wait() + }) + + t.Run("probe failure", func(t *testing.T) { + mt := newMultiPeerSyncTester(t) + mt.addPeers(10) + mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() + mt.syncBase.EXPECT().Probe(gomock.Any(), gomock.Any()). + Return(rangesync.ProbeResult{}, errors.New("probe failed")) + pl := mt.expectProbe(5, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) + // just 5 peers for which the probe worked will be checked + mt.expectFullSync(pl, 5, 0) + mt.syncBase.EXPECT().Wait().Times(2) + ctx := mt.start() + mt.clock.BlockUntilContext(ctx, 1) + }) + + t.Run("failed peers during full sync", func(t *testing.T) { + mt := newMultiPeerSyncTester(t) + mt.addPeers(10) + mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() + var ctx context.Context + for i := 0; i < numSyncs; i++ { + pl := mt.expectProbe(6, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) + mt.expectFullSync(pl, 6, 3) + mt.syncBase.EXPECT().Wait() + if i == 0 { + //nolint:fatcontext + ctx = mt.start() + } else { + mt.clock.Advance(time.Minute) + } + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + } + mt.syncBase.EXPECT().Wait() + }) + + t.Run("failed synced key handling during full sync", func(t *testing.T) { + mt := newMultiPeerSyncTester(t) + mt.addPeers(10) + mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() + var ctx context.Context + for i := 0; i < numSyncs; i++ { + pl := mt.expectProbe(6, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) + mt.expectFullSync(pl, 6, 0) + mt.syncBase.EXPECT().Wait().Return(errors.New("some handlers failed")) + if i == 0 { + //nolint:fatcontext + ctx = mt.start() + } else { + mt.clock.Advance(time.Minute) + } + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + } + mt.syncBase.EXPECT().Wait() + }) + + t.Run("cancellation during sync", func(t *testing.T) { + mt := newMultiPeerSyncTester(t) + mt.addPeers(10) + mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() + mt.expectProbe(6, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) + mt.syncRunner.EXPECT().FullSync(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, peers []p2p.Peer) error { + mt.cancel() + return ctx.Err() + }) + mt.syncBase.EXPECT().Wait().Times(2) + ctx := mt.start() + mt.clock.BlockUntilContext(ctx, 1) + require.ErrorIs(t, mt.eg.Wait(), context.Canceled) + }) +} diff --git a/sync2/multipeer/setsyncbase.go b/sync2/multipeer/setsyncbase.go new file mode 100644 index 0000000000..f36ac2c87f --- /dev/null +++ b/sync2/multipeer/setsyncbase.go @@ -0,0 +1,182 @@ +package multipeer + +import ( + "context" + "errors" + "fmt" + "io" + "sync" + + "golang.org/x/sync/singleflight" + + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +// SetSyncBase is a synchronization base which holds the original OrderedSet. +// For each peer, a Syncer is derived from the base which is used to synchronize against +// that peer only. This way, there's no propagation of any keys for which the actual data +// has not been yet received and validated. +type SetSyncBase struct { + mtx sync.Mutex + ps PairwiseSyncer + os OrderedSet + handler SyncKeyHandler + waiting []<-chan singleflight.Result + g singleflight.Group +} + +var _ SyncBase = &SetSyncBase{} + +// NewSetSyncBase creates a new SetSyncBase. +func NewSetSyncBase(ps PairwiseSyncer, os OrderedSet, handler SyncKeyHandler) *SetSyncBase { + return &SetSyncBase{ + ps: ps, + os: os, + handler: handler, + } +} + +// Count implements SyncBase. +func (ssb *SetSyncBase) Count() (int, error) { + // TODO: don't lock on db-bound operations + ssb.mtx.Lock() + defer ssb.mtx.Unlock() + if empty, err := ssb.os.Empty(); err != nil { + return 0, fmt.Errorf("check if the set is empty: %w", err) + } else if empty { + return 0, nil + } + x, err := ssb.os.Items().First() + if err != nil { + return 0, fmt.Errorf("get first item: %w", err) + } + info, err := ssb.os.GetRangeInfo(x, x) + if err != nil { + return 0, err + } + return info.Count, nil +} + +// Derive implements SyncBase. +func (ssb *SetSyncBase) Derive(p p2p.Peer) Syncer { + ssb.mtx.Lock() + defer ssb.mtx.Unlock() + return &setSyncer{ + SetSyncBase: ssb, + OrderedSet: ssb.os.Copy(true).(OrderedSet), + p: p, + handler: ssb.handler, + } +} + +// Probe implements SyncBase. +func (ssb *SetSyncBase) Probe(ctx context.Context, p p2p.Peer) (rangesync.ProbeResult, error) { + // Use a snapshot of the store to avoid holding the mutex for a long time + ssb.mtx.Lock() + os := ssb.os.Copy(true) + ssb.mtx.Unlock() + defer os.(OrderedSet).Release() + + pr, err := ssb.ps.Probe(ctx, p, os, nil, nil) + if err != nil { + return rangesync.ProbeResult{}, err + } + return pr, os.(OrderedSet).Release() +} + +func (ssb *SetSyncBase) receiveKey(k rangesync.KeyBytes, p p2p.Peer) error { + ssb.mtx.Lock() + defer ssb.mtx.Unlock() + key := k.String() + has, err := ssb.os.Has(k) + if err != nil { + return err + } + if !has { + ssb.waiting = append(ssb.waiting, + ssb.g.DoChan(key, func() (any, error) { + addToOrig, err := ssb.handler.Receive(k, p) + if err == nil && addToOrig { + ssb.mtx.Lock() + defer ssb.mtx.Unlock() + err = ssb.os.Receive(k) + } + return key, err + })) + } + return nil +} + +// Wait waits for all the handlers used by derived syncers to finish. +func (ssb *SetSyncBase) Wait() error { + // At this point, the derived syncers should be done syncing, and we only want to + // wait for the remaining handlers to complete. In case if some syncers happen to + // be still running at this point, let's not fail too badly. + // TODO: wait for any derived running syncers here, too + ssb.mtx.Lock() + waiting := ssb.waiting + ssb.waiting = nil + ssb.mtx.Unlock() + var errs []error + for _, w := range waiting { + r := <-w + ssb.g.Forget(r.Val.(string)) + errs = append(errs, r.Err) + } + return errors.Join(errs...) +} + +func (ssb *SetSyncBase) advance() error { + ssb.mtx.Lock() + defer ssb.mtx.Unlock() + return ssb.os.Advance() +} + +type setSyncer struct { + *SetSyncBase + OrderedSet + p p2p.Peer + handler SyncKeyHandler +} + +var ( + _ Syncer = &setSyncer{} + _ OrderedSet = &setSyncer{} +) + +// Peer implements Syncer. +func (ss *setSyncer) Peer() p2p.Peer { + return ss.p +} + +// Sync implements Syncer. +func (ss *setSyncer) Sync(ctx context.Context, x, y rangesync.KeyBytes) error { + if err := ss.ps.Sync(ctx, ss.p, ss, x, y); err != nil { + return err + } + return ss.commit() +} + +// Serve implements Syncer. +func (ss *setSyncer) Serve(ctx context.Context, stream io.ReadWriter) error { + if err := ss.ps.Serve(ctx, stream, ss); err != nil { + return err + } + return ss.commit() +} + +// Receive implements OrderedSet. +func (ss *setSyncer) Receive(k rangesync.KeyBytes) error { + if err := ss.receiveKey(k, ss.p); err != nil { + return err + } + return ss.OrderedSet.Receive(k) +} + +func (ss *setSyncer) commit() error { + if err := ss.handler.Commit(ss.p, ss.SetSyncBase.os, ss.OrderedSet); err != nil { + return err + } + return ss.SetSyncBase.advance() +} diff --git a/sync2/multipeer/setsyncbase_test.go b/sync2/multipeer/setsyncbase_test.go new file mode 100644 index 0000000000..ccde7f8ac9 --- /dev/null +++ b/sync2/multipeer/setsyncbase_test.go @@ -0,0 +1,273 @@ +package multipeer_test + +import ( + "context" + "errors" + "sync" + "testing" + + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/sync2/multipeer" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +type setSyncBaseTester struct { + *testing.T + ctrl *gomock.Controller + ps *MockPairwiseSyncer + handler *MockSyncKeyHandler + os *MockOrderedSet + ssb *multipeer.SetSyncBase + waitMtx sync.Mutex + waitChs map[string]chan error + doneCh chan rangesync.KeyBytes +} + +func newSetSyncBaseTester(t *testing.T, os multipeer.OrderedSet) *setSyncBaseTester { + ctrl := gomock.NewController(t) + st := &setSyncBaseTester{ + T: t, + ctrl: ctrl, + ps: NewMockPairwiseSyncer(ctrl), + waitChs: make(map[string]chan error), + doneCh: make(chan rangesync.KeyBytes), + } + if os == nil { + st.os = NewMockOrderedSet(ctrl) + st.os.EXPECT().Items().DoAndReturn(func() rangesync.SeqResult { + return rangesync.EmptySeqResult() + }).AnyTimes() + os = st.os + } + st.handler = NewMockSyncKeyHandler(ctrl) + st.handler.EXPECT().Receive(gomock.Any(), gomock.Any()). + DoAndReturn(func(k rangesync.KeyBytes, p p2p.Peer) (bool, error) { + err := <-st.getWaitCh(k) + st.doneCh <- k + return true, err + }).AnyTimes() + st.ssb = multipeer.NewSetSyncBase(st.ps, os, st.handler) + return st +} + +func (st *setSyncBaseTester) getWaitCh(k rangesync.KeyBytes) chan error { + st.waitMtx.Lock() + defer st.waitMtx.Unlock() + ch, found := st.waitChs[string(k)] + if !found { + ch = make(chan error) + st.waitChs[string(k)] = ch + } + return ch +} + +func (st *setSyncBaseTester) expectCopy(addedKeys ...rangesync.KeyBytes) *MockOrderedSet { + copy := NewMockOrderedSet(st.ctrl) + st.os.EXPECT().Copy(true).DoAndReturn(func(bool) rangesync.OrderedSet { + copy.EXPECT().Items().DoAndReturn(func() rangesync.SeqResult { + return rangesync.EmptySeqResult() + }).AnyTimes() + for _, k := range addedKeys { + copy.EXPECT().Receive(k) + } + // TODO: do better job at tracking Release() calls + copy.EXPECT().Release().AnyTimes() + return copy + }) + return copy +} + +func (st *setSyncBaseTester) expectSync( + p p2p.Peer, + ss multipeer.Syncer, + addedKeys ...rangesync.KeyBytes, +) { + st.ps.EXPECT().Sync(gomock.Any(), p, ss, nil, nil). + DoAndReturn(func( + _ context.Context, + p p2p.Peer, + os rangesync.OrderedSet, + x, y rangesync.KeyBytes, + ) error { + for _, k := range addedKeys { + require.NoError(st, os.Receive(k)) + } + return nil + }) +} + +func (st *setSyncBaseTester) wait(count int) ([]rangesync.KeyBytes, error) { + var eg errgroup.Group + eg.Go(st.ssb.Wait) + var handledKeys []rangesync.KeyBytes + for k := range st.doneCh { + handledKeys = append(handledKeys, k.Clone()) + count-- + if count == 0 { + break + } + } + return handledKeys, eg.Wait() +} + +func TestSetSyncBase(t *testing.T) { + t.Run("probe", func(t *testing.T) { + t.Parallel() + st := newSetSyncBaseTester(t, nil) + expPr := rangesync.ProbeResult{ + FP: rangesync.RandomFingerprint(), + Count: 42, + Sim: 0.99, + } + set := st.expectCopy() + st.ps.EXPECT().Probe(gomock.Any(), p2p.Peer("p1"), set, nil, nil).Return(expPr, nil) + pr, err := st.ssb.Probe(context.Background(), p2p.Peer("p1")) + require.NoError(t, err) + require.Equal(t, expPr, pr) + }) + + t.Run("single key one-time sync", func(t *testing.T) { + t.Parallel() + st := newSetSyncBaseTester(t, nil) + + addedKey := rangesync.RandomKeyBytes(32) + st.expectCopy(addedKey) + ss := st.ssb.Derive(p2p.Peer("p1")) + require.Equal(t, p2p.Peer("p1"), ss.Peer()) + + x := rangesync.RandomKeyBytes(32) + y := rangesync.RandomKeyBytes(32) + st.handler.EXPECT().Commit(gomock.Any(), gomock.Any(), gomock.Any()) + st.os.EXPECT().Advance() + st.ps.EXPECT().Sync(gomock.Any(), p2p.Peer("p1"), ss, x, y) + require.NoError(t, ss.Sync(context.Background(), x, y)) + + st.os.EXPECT().Has(addedKey) + st.os.EXPECT().Receive(addedKey) + st.expectSync(p2p.Peer("p1"), ss, addedKey) + st.handler.EXPECT().Commit(gomock.Any(), gomock.Any(), gomock.Any()) + st.os.EXPECT().Advance() + require.NoError(t, ss.Sync(context.Background(), nil, nil)) + close(st.getWaitCh(addedKey)) + + handledKeys, err := st.wait(1) + require.NoError(t, err) + require.ElementsMatch(t, []rangesync.KeyBytes{addedKey}, handledKeys) + }) + + t.Run("single key synced multiple times", func(t *testing.T) { + t.Parallel() + st := newSetSyncBaseTester(t, nil) + + addedKey := rangesync.RandomKeyBytes(32) + st.expectCopy(addedKey, addedKey, addedKey) + ss := st.ssb.Derive(p2p.Peer("p1")) + require.Equal(t, p2p.Peer("p1"), ss.Peer()) + + // added just once + st.os.EXPECT().Receive(addedKey) + for i := 0; i < 3; i++ { + st.os.EXPECT().Has(addedKey) + st.expectSync(p2p.Peer("p1"), ss, addedKey) + st.handler.EXPECT().Commit(gomock.Any(), gomock.Any(), gomock.Any()) + st.os.EXPECT().Advance() + require.NoError(t, ss.Sync(context.Background(), nil, nil)) + } + close(st.getWaitCh(addedKey)) + + handledKeys, err := st.wait(1) + require.NoError(t, err) + require.ElementsMatch(t, []rangesync.KeyBytes{addedKey}, handledKeys) + }) + + t.Run("multiple keys", func(t *testing.T) { + t.Parallel() + st := newSetSyncBaseTester(t, nil) + + k1 := rangesync.RandomKeyBytes(32) + k2 := rangesync.RandomKeyBytes(32) + st.expectCopy(k1, k2) + ss := st.ssb.Derive(p2p.Peer("p1")) + require.Equal(t, p2p.Peer("p1"), ss.Peer()) + + st.os.EXPECT().Has(k1) + st.os.EXPECT().Has(k2) + st.os.EXPECT().Receive(k1) + st.os.EXPECT().Receive(k2) + st.expectSync(p2p.Peer("p1"), ss, k1, k2) + st.handler.EXPECT().Commit(gomock.Any(), gomock.Any(), gomock.Any()) + st.os.EXPECT().Advance() + require.NoError(t, ss.Sync(context.Background(), nil, nil)) + close(st.getWaitCh(k1)) + close(st.getWaitCh(k2)) + + handledKeys, err := st.wait(2) + require.NoError(t, err) + require.ElementsMatch(t, []rangesync.KeyBytes{k1, k2}, handledKeys) + }) + + t.Run("handler failure", func(t *testing.T) { + t.Parallel() + st := newSetSyncBaseTester(t, nil) + + k1 := rangesync.RandomKeyBytes(32) + k2 := rangesync.RandomKeyBytes(32) + st.expectCopy(k1, k2) + ss := st.ssb.Derive(p2p.Peer("p1")) + require.Equal(t, p2p.Peer("p1"), ss.Peer()) + + st.os.EXPECT().Has(k1) + st.os.EXPECT().Has(k2) + // k1 is not propagated to syncBase due to the handler failure + st.os.EXPECT().Receive(k2) + st.expectSync(p2p.Peer("p1"), ss, k1, k2) + st.handler.EXPECT().Commit(gomock.Any(), gomock.Any(), gomock.Any()) + st.os.EXPECT().Advance() + require.NoError(t, ss.Sync(context.Background(), nil, nil)) + handlerErr := errors.New("fail") + st.getWaitCh(k1) <- handlerErr + close(st.getWaitCh(k2)) + + handledKeys, err := st.wait(2) + require.ErrorIs(t, err, handlerErr) + require.ElementsMatch(t, []rangesync.KeyBytes{k1, k2}, handledKeys) + }) + + t.Run("real item set", func(t *testing.T) { + t.Parallel() + hs := make([]rangesync.KeyBytes, 4) + for n := range hs { + hs[n] = rangesync.RandomKeyBytes(32) + } + os := multipeer.NewDumbHashSet() + os.AddUnchecked(hs[0]) + os.AddUnchecked(hs[1]) + st := newSetSyncBaseTester(t, os) + ss := st.ssb.Derive(p2p.Peer("p1")) + ss.(rangesync.OrderedSet).Receive(hs[2]) + ss.(rangesync.OrderedSet).Add(hs[2]) + ss.(rangesync.OrderedSet).Receive(hs[3]) + ss.(rangesync.OrderedSet).Add(hs[3]) + // syncer's cloned ItemStore has new key immediately + has, err := ss.(multipeer.OrderedSet).Has(hs[2]) + require.NoError(t, err) + require.True(t, has) + has, err = ss.(multipeer.OrderedSet).Has(hs[3]) + require.NoError(t, err) + require.True(t, has) + handlerErr := errors.New("fail") + st.getWaitCh(hs[2]) <- handlerErr + close(st.getWaitCh(hs[3])) + handledKeys, err := st.wait(2) + require.ErrorIs(t, err, handlerErr) + require.ElementsMatch(t, hs[2:], handledKeys) + // only successfully handled keys propagate the syncBase + received, err := os.Received().Collect() + require.NoError(t, err) + require.ElementsMatch(t, hs[3:], received) + }) +} diff --git a/sync2/multipeer/split_sync.go b/sync2/multipeer/split_sync.go new file mode 100644 index 0000000000..ae9c44c0c8 --- /dev/null +++ b/sync2/multipeer/split_sync.go @@ -0,0 +1,206 @@ +package multipeer + +import ( + "context" + "errors" + "slices" + "time" + + "github.com/jonboulle/clockwork" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/fetch/peers" + "github.com/spacemeshos/go-spacemesh/p2p" +) + +type syncResult struct { + s Syncer + err error +} + +// splitSync is a synchronization implementation that synchronizes the set against +// multiple peers in parallel, but splits the synchronization into ranges and assigns +// each range to a single peer. +// The splitting is done in such way that only maxDepth high bits of the key are non-zero, +// which helps with radix tree based OrderedSet implementation. +type splitSync struct { + logger *zap.Logger + syncBase SyncBase + peers *peers.Peers + syncPeers []p2p.Peer + gracePeriod time.Duration + clock clockwork.Clock + sq syncQueue + resCh chan syncResult + slowRangeCh chan *syncRange + syncMap map[p2p.Peer]*syncRange + failedPeers map[p2p.Peer]struct{} + numRunning int + numRemaining int + numPeers int + eg *errgroup.Group +} + +func newSplitSync( + logger *zap.Logger, + syncBase SyncBase, + peers *peers.Peers, + syncPeers []p2p.Peer, + gracePeriod time.Duration, + clock clockwork.Clock, + keyLen, maxDepth int, +) *splitSync { + if len(syncPeers) == 0 { + panic("BUG: no peers passed to splitSync") + } + return &splitSync{ + logger: logger, + syncBase: syncBase, + peers: peers, + syncPeers: syncPeers, + gracePeriod: gracePeriod, + clock: clock, + sq: newSyncQueue(len(syncPeers), keyLen, maxDepth), + // TODO: should not need buffering (stop when finished) + resCh: make(chan syncResult, 3*len(syncPeers)), + syncMap: make(map[p2p.Peer]*syncRange), + failedPeers: make(map[p2p.Peer]struct{}), + numRemaining: len(syncPeers), + numPeers: len(syncPeers), + // TODO: should not need buffering (stop when finished) + slowRangeCh: make(chan *syncRange, 3*len(syncPeers)), + } +} + +func (s *splitSync) nextPeer() p2p.Peer { + if len(s.syncPeers) == 0 { + panic("BUG: no peers") + } + p := s.syncPeers[0] + s.syncPeers = s.syncPeers[1:] + return p +} + +func (s *splitSync) startPeerSync(ctx context.Context, p p2p.Peer, sr *syncRange) { + syncer := s.syncBase.Derive(p) + sr.NumSyncers++ + s.numRunning++ + doneCh := make(chan struct{}) + s.eg.Go(func() error { + defer func() { + syncer.Release() + close(doneCh) + }() + err := syncer.Sync(ctx, sr.X, sr.Y) + select { + case <-ctx.Done(): + return ctx.Err() + case s.resCh <- syncResult{s: syncer, err: err}: + return syncer.Release() + } + }) + gpTimer := s.clock.After(s.gracePeriod) + s.eg.Go(func() error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-doneCh: + case <-gpTimer: + // if another peer finishes its part early, let + // it pick up this range + s.slowRangeCh <- sr + } + return nil + }) +} + +func (s *splitSync) handleSyncResult(r syncResult) error { + sr, found := s.syncMap[r.s.Peer()] + if !found { + panic("BUG: error in split sync syncMap handling") + } + s.numRunning-- + delete(s.syncMap, r.s.Peer()) + sr.NumSyncers-- + if r.err != nil { + s.numPeers-- + s.failedPeers[r.s.Peer()] = struct{}{} + s.logger.Debug("remove failed peer", + zap.Stringer("peer", r.s.Peer()), + zap.Int("numPeers", s.numPeers), + zap.Int("numRemaining", s.numRemaining), + zap.Int("numRunning", s.numRunning), + zap.Int("availPeers", len(s.syncPeers))) + if s.numPeers == 0 && s.numRemaining != 0 { + return errors.New("all peers dropped before full sync has completed") + } + if sr.NumSyncers == 0 { + // prioritize the syncRange for resync after failed + // sync with no active syncs remaining + s.sq.Update(sr, time.Time{}) + } + } else { + sr.Done = true + s.syncPeers = append(s.syncPeers, r.s.Peer()) + s.numRemaining-- + s.logger.Debug("peer synced successfully", + zap.Stringer("peer", r.s.Peer()), + zap.Int("numPeers", s.numPeers), + zap.Int("numRemaining", s.numRemaining), + zap.Int("numRunning", s.numRunning), + zap.Int("availPeers", len(s.syncPeers))) + } + + return nil +} + +func (s *splitSync) clearDeadPeers() { + s.syncPeers = slices.DeleteFunc(s.syncPeers, func(p p2p.Peer) bool { + if !s.peers.Contains(p) { + return true + } + _, failed := s.failedPeers[p] + return failed + }) +} + +func (s *splitSync) Sync(ctx context.Context) error { + sctx, cancel := context.WithCancel(ctx) + defer cancel() + var syncCtx context.Context + s.eg, syncCtx = errgroup.WithContext(sctx) + for s.numRemaining > 0 { + var sr *syncRange + for { + sr := s.sq.PopRange() + if sr != nil { + if sr.Done { + continue + } + p := s.nextPeer() + s.syncMap[p] = sr + s.startPeerSync(syncCtx, p, sr) + } + break + } + s.clearDeadPeers() + for s.numRemaining > 0 && (s.sq.empty() || len(s.syncPeers) == 0) { + if s.numRunning == 0 && len(s.syncPeers) == 0 { + return errors.New("all peers dropped before full sync has completed") + } + select { + case sr = <-s.slowRangeCh: + // push this syncRange to the back of the queue + s.sq.Update(sr, s.clock.Now()) + case <-syncCtx.Done(): + return syncCtx.Err() + case r := <-s.resCh: + if err := s.handleSyncResult(r); err != nil { + return err + } + } + } + } + return s.eg.Wait() +} diff --git a/sync2/multipeer/split_sync_test.go b/sync2/multipeer/split_sync_test.go new file mode 100644 index 0000000000..e10aedabe8 --- /dev/null +++ b/sync2/multipeer/split_sync_test.go @@ -0,0 +1,150 @@ +package multipeer_test + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/fetch/peers" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/sync2/multipeer" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +type splitSyncTester struct { + testing.TB + + syncPeers []p2p.Peer + clock clockwork.Clock + mtx sync.Mutex + fail map[hexRange]bool + expPeerRanges map[hexRange]int + peerRanges map[hexRange][]p2p.Peer + syncBase *MockSyncBase + peers *peers.Peers + splitSync *multipeer.SplitSync +} + +var tstRanges = []hexRange{ + { + "0000000000000000000000000000000000000000000000000000000000000000", + "4000000000000000000000000000000000000000000000000000000000000000", + }, + { + "4000000000000000000000000000000000000000000000000000000000000000", + "8000000000000000000000000000000000000000000000000000000000000000", + }, + { + "8000000000000000000000000000000000000000000000000000000000000000", + "c000000000000000000000000000000000000000000000000000000000000000", + }, + { + "c000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + }, +} + +func newTestSplitSync(t testing.TB) *splitSyncTester { + ctrl := gomock.NewController(t) + tst := &splitSyncTester{ + syncPeers: make([]p2p.Peer, 4), + clock: clockwork.NewFakeClock(), + fail: make(map[hexRange]bool), + expPeerRanges: map[hexRange]int{ + tstRanges[0]: 0, + tstRanges[1]: 0, + tstRanges[2]: 0, + tstRanges[3]: 0, + }, + peerRanges: make(map[hexRange][]p2p.Peer), + syncBase: NewMockSyncBase(ctrl), + peers: peers.New(), + } + for n := range tst.syncPeers { + tst.syncPeers[n] = p2p.Peer(types.RandomBytes(20)) + } + for index, p := range tst.syncPeers { + tst.syncBase.EXPECT(). + Derive(p). + DoAndReturn(func(peer p2p.Peer) multipeer.Syncer { + s := NewMockSyncer(ctrl) + s.EXPECT().Peer().Return(p).AnyTimes() + // TODO: do better job at tracking Release() calls + s.EXPECT().Release().AnyTimes() + s.EXPECT(). + Sync(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, x, y rangesync.KeyBytes) error { + tst.mtx.Lock() + defer tst.mtx.Unlock() + require.NotNil(t, x) + require.NotNil(t, y) + k := hexRange{x.String(), y.String()} + tst.peerRanges[k] = append(tst.peerRanges[k], peer) + count, found := tst.expPeerRanges[k] + require.True(t, found, "peer range not found: x %s y %s", x, y) + if tst.fail[k] { + t.Logf("ERR: peer %d x %s y %s", + index, x.String(), y.String()) + tst.fail[k] = false + return errors.New("injected fault") + } else { + t.Logf("OK: peer %d x %s y %s", + index, x.String(), y.String()) + tst.expPeerRanges[k] = count + 1 + } + return nil + }) + return s + }). + AnyTimes() + } + for _, p := range tst.syncPeers { + tst.peers.Add(p) + } + tst.splitSync = multipeer.NewSplitSync( + zaptest.NewLogger(t), + tst.syncBase, + tst.peers, + tst.syncPeers, + time.Minute, + tst.clock, + 32, 24, + ) + return tst +} + +func TestSplitSync(t *testing.T) { + tst := newTestSplitSync(t) + var eg errgroup.Group + eg.Go(func() error { + return tst.splitSync.Sync(context.Background()) + }) + require.NoError(t, eg.Wait()) + for pr, count := range tst.expPeerRanges { + require.Equal(t, 1, count, "bad sync count: x %s y %s", pr[0], pr[1]) + } +} + +func TestSplitSyncRetry(t *testing.T) { + tst := newTestSplitSync(t) + tst.fail[tstRanges[1]] = true + tst.fail[tstRanges[2]] = true + var eg errgroup.Group + eg.Go(func() error { + return tst.splitSync.Sync(context.Background()) + }) + require.NoError(t, eg.Wait()) + for pr, count := range tst.expPeerRanges { + require.False(t, tst.fail[pr], "fail cleared for x %s y %s", pr[0], pr[1]) + require.Equal(t, 1, count, "peer range not synced: x %s y %s", pr[0], pr[1]) + } +} diff --git a/sync2/multipeer/sync_queue.go b/sync2/multipeer/sync_queue.go new file mode 100644 index 0000000000..d925d4ccc2 --- /dev/null +++ b/sync2/multipeer/sync_queue.go @@ -0,0 +1,108 @@ +package multipeer + +import ( + "container/heap" + "time" + + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +// syncRange represents a range of keys to be synchronized, along with the status +// information. +type syncRange struct { + X, Y rangesync.KeyBytes + LastSyncStarted time.Time + Done bool + NumSyncers int + Index int +} + +// syncQueue is a priority queue for syncRanges. +type syncQueue []*syncRange + +// Len implements heap.Interface. +func (sq syncQueue) Len() int { return len(sq) } + +// Less implements heap.Interface. +func (sq syncQueue) Less(i, j int) bool { + // We want Pop to give us syncRange for which which sync has started the + // earliest. Items which are not being synced are considered "most earliest" + return sq[i].LastSyncStarted.Before(sq[j].LastSyncStarted) +} + +// Swap implements heap.Interface. +func (sq syncQueue) Swap(i, j int) { + sq[i], sq[j] = sq[j], sq[i] + sq[i].Index = i + sq[j].Index = j +} + +// Push implements heap.Interface. +func (sq *syncQueue) Push(i any) { + n := len(*sq) + sr := i.(*syncRange) + sr.Index = n + *sq = append(*sq, sr) +} + +// Pop implements heap.Interface. +func (sq *syncQueue) Pop() any { + old := *sq + n := len(old) + sr := old[n-1] + old[n-1] = nil // avoid memory leak + sr.Index = -1 // not in the queue anymore + *sq = old[0 : n-1] + return sr +} + +func newSyncQueue(numPeers, keyLen, maxDepth int) syncQueue { + delim := getDelimiters(numPeers, keyLen, maxDepth) + y := make(rangesync.KeyBytes, keyLen) + sq := make(syncQueue, numPeers) + for n := range sq { + x := y.Clone() + if n < numPeers-1 { + y = delim[n] + } else { + y = make(rangesync.KeyBytes, keyLen) + } + sq[n] = &syncRange{ + X: x, + Y: y, + } + } + heap.Init(&sq) + return sq +} + +func (sq *syncQueue) empty() bool { + return len(*sq) == 0 +} + +func (sq *syncQueue) PopRange() *syncRange { + if sq.empty() { + return nil + } + sr := heap.Pop(sq).(*syncRange) + sr.Index = -1 + return sr +} + +func (sq *syncQueue) PushRange(sr *syncRange) { + if sr.Done { + panic("BUG: pushing a finished syncRange into the queue") + } + if sr.Index == -1 { + heap.Push(sq, sr) + } +} + +func (sq *syncQueue) Update(sr *syncRange, lastSyncStarted time.Time) { + sr.LastSyncStarted = lastSyncStarted + if sr.Index == -1 { + sq.PushRange(sr) + } else { + heap.Fix(sq, sr.Index) + } +} diff --git a/sync2/multipeer/sync_queue_test.go b/sync2/multipeer/sync_queue_test.go new file mode 100644 index 0000000000..ae0ccd5827 --- /dev/null +++ b/sync2/multipeer/sync_queue_test.go @@ -0,0 +1,70 @@ +package multipeer_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/sync2/multipeer" +) + +type hexRange [2]string + +func TestSyncQueue(t *testing.T) { + expPeerRanges := map[hexRange]bool{ + { + "0000000000000000000000000000000000000000000000000000000000000000", + "4000000000000000000000000000000000000000000000000000000000000000", + }: false, + { + "4000000000000000000000000000000000000000000000000000000000000000", + "8000000000000000000000000000000000000000000000000000000000000000", + }: false, + { + "8000000000000000000000000000000000000000000000000000000000000000", + "c000000000000000000000000000000000000000000000000000000000000000", + }: false, + { + "c000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + }: false, + } + sq := multipeer.NewSyncQueue(4, 32, 24) + startTime := time.Now() + pushed := make([]hexRange, 4) + for i := 0; i < 4; i++ { + sr := sq.PopRange() + require.NotNil(t, sr) + require.True(t, sr.LastSyncStarted.IsZero()) + require.False(t, sr.Done) + require.Zero(t, sr.NumSyncers) + k := hexRange{sr.X.String(), sr.Y.String()} + processed, found := expPeerRanges[k] + require.True(t, found) + require.False(t, processed) + expPeerRanges[k] = true + t.Logf("push range %v at %v", k, sr.LastSyncStarted) + if i != 1 { + sr.LastSyncStarted = startTime + sq.PushRange(sr) // pushed to the end + } else { + // use update for one of the items + // instead of pushing with proper time + sq.Update(sr, startTime) + } + if i == 0 { + sq.PushRange(sr) // should do nothing + } + startTime = startTime.Add(10 * time.Second) + pushed[i] = k + } + require.Len(t, sq, 4) + for i := 0; i < 4; i++ { + sr := sq.PopRange() + k := hexRange{sr.X.String(), sr.Y.String()} + t.Logf("pop range %v at %v", k, sr.LastSyncStarted) + require.Equal(t, pushed[i], k) + } + require.Empty(t, sq) +} diff --git a/sync2/multipeer/synclist.go b/sync2/multipeer/synclist.go new file mode 100644 index 0000000000..fd7ad92c2f --- /dev/null +++ b/sync2/multipeer/synclist.go @@ -0,0 +1,60 @@ +package multipeer + +import ( + "container/list" + "sync" + "time" + + "github.com/jonboulle/clockwork" +) + +// syncList keeps track of recent full syncs and reports whether the node is synced, that +// is, the specified number of syncs has happened within the specified duration of time. +type syncList struct { + mtx sync.Mutex + clock clockwork.Clock + minSyncCount int + duration time.Duration + syncs list.List +} + +func newSyncList(clock clockwork.Clock, minSyncCount int, duration time.Duration) *syncList { + return &syncList{ + clock: clock, + minSyncCount: minSyncCount, + duration: duration, + } +} + +func (sl *syncList) prune(now time.Time) { + t := now.Add(-sl.duration) + for sl.syncs.Len() != 0 { + el := sl.syncs.Back() + if t.After(el.Value.(time.Time)) { + sl.syncs.Remove(el) + } else { + break + } + } +} + +func (sl *syncList) NoteSync() { + sl.mtx.Lock() + defer sl.mtx.Unlock() + now := sl.clock.Now() + sl.prune(now) + sl.syncs.PushFront(now) +} + +func (sl *syncList) Synced() bool { + sl.mtx.Lock() + defer sl.mtx.Unlock() + sl.prune(sl.clock.Now()) + return sl.syncs.Len() >= sl.minSyncCount +} + +func (sl *syncList) Len() int { + sl.mtx.Lock() + defer sl.mtx.Unlock() + return sl.syncs.Len() +} diff --git a/sync2/multipeer/synclist_test.go b/sync2/multipeer/synclist_test.go new file mode 100644 index 0000000000..b8ff0a0d06 --- /dev/null +++ b/sync2/multipeer/synclist_test.go @@ -0,0 +1,35 @@ +package multipeer_test + +import ( + "testing" + "time" + + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/sync2/multipeer" +) + +func TestSyncList(t *testing.T) { + clk := clockwork.NewFakeClock() + sl := multipeer.NewSyncList(clk, 3, 5*time.Minute) + require.False(t, sl.Synced()) + sl.NoteSync() + require.False(t, sl.Synced()) + clk.Advance(time.Minute) + sl.NoteSync() + require.False(t, sl.Synced()) + clk.Advance(time.Minute) + sl.NoteSync() + require.True(t, sl.Synced()) + clk.Advance(time.Minute) + // 3 minutes have passed + require.True(t, sl.Synced()) + clk.Advance(2*time.Minute + 30*time.Second) + // 5 minutes 30 s have passed + require.False(t, sl.Synced()) + sl.NoteSync() + require.True(t, sl.Synced()) + // make sure the list is pruned and is not growing indefinitely + require.Equal(t, 3, sl.Len()) +} diff --git a/sync2/p2p.go b/sync2/p2p.go new file mode 100644 index 0000000000..562bbf3b4e --- /dev/null +++ b/sync2/p2p.go @@ -0,0 +1,211 @@ +package sync2 + +import ( + "context" + "errors" + "fmt" + "io" + "sync" + "sync/atomic" + "time" + + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/fetch/peers" + "github.com/spacemeshos/go-spacemesh/p2p/server" + "github.com/spacemeshos/go-spacemesh/sync2/multipeer" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +type Dispatcher = rangesync.Dispatcher + +// Config contains the configuration for the P2PHashSync. +type Config struct { + MaxSendRange int `mapstructure:"max-send-range"` + SampleSize int `mapstructure:"sample-size"` + SyncPeerCount int `mapstructure:"sync-peer-count"` + MinSplitSyncCount int `mapstructure:"min-split-sync-count"` + MaxFullDiff int `mapstructure:"max-full-diff"` + SyncInterval time.Duration `mapstructure:"sync-interval"` + NoPeersRecheckInterval time.Duration `mapstructure:"no-peers-recheck-interval"` + MinSplitSyncPeers int `mapstructure:"min-split-sync-peers"` + MinCompleteFraction float64 `mapstructure:"min-complete-fraction"` + SplitSyncGracePeriod time.Duration `mapstructure:"split-sync-grace-period"` + RecentTimeSpan time.Duration `mapstructure:"recent-time-span"` + EnableActiveSync bool `mapstructure:"enable-active-sync"` + MaxReconcDiff float64 `mapstructure:"max-reconc-diff"` + AutoCommitCount int `mapstructure:"auto-commit-count"` + AutoCommitIdle time.Duration `mapstructure:"auto-commit-idle"` + TrafficLimit int `mapstructure:"traffic-limit"` + MessageLimit int `mapstructure:"message-limit"` +} + +// DefaultConfig returns the default configuration for the P2PHashSync. +func DefaultConfig() Config { + return Config{ + MaxSendRange: rangesync.DefaultMaxSendRange, + SampleSize: rangesync.DefaultSampleSize, + SyncPeerCount: 20, + MinSplitSyncPeers: 2, + MinSplitSyncCount: 1000, + MaxFullDiff: 10000, + SyncInterval: 5 * time.Minute, + MinCompleteFraction: 0.5, + SplitSyncGracePeriod: time.Minute, + NoPeersRecheckInterval: 30 * time.Second, + MaxReconcDiff: 0.01, + AutoCommitCount: 10000, + AutoCommitIdle: time.Second, + TrafficLimit: 200_000_000, + MessageLimit: 20_000_000, + } +} + +// P2PHashSync is handles the synchronization of a local OrderedSet against other peers. +type P2PHashSync struct { + logger *zap.Logger + cfg Config + os multipeer.OrderedSet + syncBase multipeer.SyncBase + reconciler *multipeer.MultiPeerReconciler + cancel context.CancelFunc + eg errgroup.Group + start sync.Once + running atomic.Bool +} + +// NewP2PHashSync creates a new P2PHashSync. +func NewP2PHashSync( + logger *zap.Logger, + d *Dispatcher, + name string, + os multipeer.OrderedSet, + keyLen, maxDepth int, + peers *peers.Peers, + handler multipeer.SyncKeyHandler, + cfg Config, + requester rangesync.Requester, +) *P2PHashSync { + s := &P2PHashSync{ + logger: logger, + os: os, + cfg: cfg, + } + rangeSyncOpts := []rangesync.RangeSetReconcilerOption{ + rangesync.WithMaxSendRange(cfg.MaxSendRange), + rangesync.WithSampleSize(cfg.SampleSize), + rangesync.WithMaxDiff(cfg.MaxReconcDiff), + rangesync.WithLogger(logger), + } + if cfg.RecentTimeSpan > 0 { + rangeSyncOpts = append(rangeSyncOpts, rangesync.WithRecentTimeSpan(cfg.RecentTimeSpan)) + } + // var ps multipeer.PairwiseSyncer + ps := rangesync.NewPairwiseSetSyncer(requester, name, rangeSyncOpts, []rangesync.ConduitOption{ + rangesync.WithTrafficLimit(cfg.TrafficLimit), + rangesync.WithMessageLimit(cfg.MessageLimit), + }) + s.syncBase = multipeer.NewSetSyncBase(ps, s.os, handler) + s.reconciler = multipeer.NewMultiPeerReconciler( + s.syncBase, peers, keyLen, maxDepth, + multipeer.WithLogger(logger), + multipeer.WithSyncPeerCount(cfg.SyncPeerCount), + multipeer.WithMinSplitSyncPeers(cfg.MinSplitSyncPeers), + multipeer.WithMinSplitSyncCount(cfg.MinSplitSyncCount), + multipeer.WithMaxFullDiff(cfg.MaxFullDiff), + multipeer.WithSyncInterval(cfg.SyncInterval), + multipeer.WithMinCompleteFraction(cfg.MinCompleteFraction), + multipeer.WithSplitSyncGracePeriod(time.Minute), + multipeer.WithNoPeersRecheckInterval(cfg.NoPeersRecheckInterval)) + d.Register(name, s.serve) + return s +} + +func (s *P2PHashSync) serve(ctx context.Context, stream io.ReadWriter) error { + peer, found := server.ContextPeerID(ctx) + if !found { + panic("BUG: no peer ID found in the handler") + } + // We derive a dedicated Syncer for the peer being served to pass all the received + // items through the handler before adding them to the main ItemStore + return s.syncBase.Derive(peer).Serve(ctx, stream) +} + +// Set returns the OrderedSet that is being synchronized. +func (s *P2PHashSync) Set() rangesync.OrderedSet { + return s.os +} + +// Load loads the OrderedSet from the underlying storage. +func (s *P2PHashSync) Load() error { + s.logger.Info("loading the set") + start := time.Now() + // We pre-load the set to avoid waiting for it to load during a + // sync request + if err := s.os.EnsureLoaded(); err != nil { + return fmt.Errorf("load set: %w", err) + } + info, err := s.os.GetRangeInfo(nil, nil) + if err != nil { + return fmt.Errorf("get range info: %w", err) + } + s.logger.Info("done loading the set", + zap.Duration("elapsed", time.Since(start)), + zap.Int("count", info.Count), + zap.Stringer("fingerprint", info.Fingerprint)) + return nil +} + +// Start starts the multi-peer reconciler. +func (s *P2PHashSync) Start() { + if !s.cfg.EnableActiveSync { + s.logger.Info("active sync is disabled") + return + } + s.running.Store(true) + s.start.Do(func() { + s.eg.Go(func() error { + defer s.running.Store(false) + var ctx context.Context + ctx, s.cancel = context.WithCancel(context.Background()) + return s.reconciler.Run(ctx) + }) + }) +} + +// Stop stops the multi-peer reconciler. +func (s *P2PHashSync) Stop() { + if !s.cfg.EnableActiveSync || !s.running.Load() { + return + } + if s.cancel != nil { + s.cancel() + } + if err := s.eg.Wait(); err != nil && !errors.Is(err, context.Canceled) { + s.logger.Error("P2PHashSync terminated with an error", zap.Error(err)) + } +} + +// Synced returns true if the local OrderedSet is in sync with the peers, as determined by +// the multi-peer reconciler. +func (s *P2PHashSync) Synced() bool { + return s.reconciler.Synced() +} + +var errStopped = errors.New("syncer stopped") + +// WaitForSync waits until the local OrderedSet is in sync with the peers. +func (s *P2PHashSync) WaitForSync(ctx context.Context) error { + for !s.Synced() { + if !s.running.Load() { + return errStopped + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(50 * time.Millisecond): + } + } + return nil +} diff --git a/sync2/p2p_test.go b/sync2/p2p_test.go new file mode 100644 index 0000000000..5116302400 --- /dev/null +++ b/sync2/p2p_test.go @@ -0,0 +1,162 @@ +package sync2_test + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + "golang.org/x/sync/errgroup" + + "github.com/spacemeshos/go-spacemesh/fetch/peers" + "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/p2p/server" + "github.com/spacemeshos/go-spacemesh/sync2" + "github.com/spacemeshos/go-spacemesh/sync2/multipeer" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +type addedKey struct { + // The fields are actually used to make sure each key is synced just once between + // each pair of peers. + //nolint:unused + fromPeer, toPeer p2p.Peer + //nolint:unused + key string +} + +type fakeHandler struct { + mtx *sync.Mutex + localPeerID p2p.Peer + synced map[addedKey]struct{} + committed map[string]struct{} +} + +func (fh *fakeHandler) Receive(k rangesync.KeyBytes, peer p2p.Peer) (bool, error) { + fh.mtx.Lock() + defer fh.mtx.Unlock() + ak := addedKey{ + toPeer: fh.localPeerID, + key: string(k), + } + fh.synced[ak] = struct{}{} + return true, nil +} + +func (fh *fakeHandler) Commit(peer p2p.Peer, base, new multipeer.OrderedSet) error { + fh.mtx.Lock() + defer fh.mtx.Unlock() + for k := range fh.synced { + fh.committed[k.key] = struct{}{} + } + clear(fh.synced) + return nil +} + +func (fh *fakeHandler) committedItems() (items []rangesync.KeyBytes) { + fh.mtx.Lock() + defer fh.mtx.Unlock() + for k := range fh.committed { + items = append(items, rangesync.KeyBytes(k)) + } + return items +} + +func TestP2P(t *testing.T) { + const ( + numNodes = 4 + numHashes = 100 + keyLen = 32 + maxDepth = 24 + ) + logger := zaptest.NewLogger(t) + mesh, err := mocknet.FullMeshConnected(numNodes) + require.NoError(t, err) + hs := make([]*sync2.P2PHashSync, numNodes) + handlers := make([]*fakeHandler, numNodes) + initialSet := make([]rangesync.KeyBytes, numHashes) + for n := range initialSet { + initialSet[n] = rangesync.RandomKeyBytes(32) + } + var eg errgroup.Group + var mtx sync.Mutex + defer eg.Wait() + for n := range hs { + ps := peers.New() + for m := 0; m < numNodes; m++ { + if m != n { + ps.Add(mesh.Hosts()[m].ID()) + } + } + cfg := sync2.DefaultConfig() + cfg.EnableActiveSync = true + cfg.SyncInterval = 100 * time.Millisecond + host := mesh.Hosts()[n] + handlers[n] = &fakeHandler{ + mtx: &mtx, + localPeerID: host.ID(), + synced: make(map[addedKey]struct{}), + committed: make(map[string]struct{}), + } + os := multipeer.NewDumbHashSet() + d := rangesync.NewDispatcher(logger) + srv := d.SetupServer(host, "sync2test", server.WithLog(logger)) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + eg.Go(func() error { return srv.Run(ctx) }) + hs[n] = sync2.NewP2PHashSync( + logger.Named(fmt.Sprintf("node%d", n)), + d, "test", os, keyLen, maxDepth, ps, handlers[n], cfg, srv) + require.NoError(t, hs[n].Load()) + is := hs[n].Set().(*multipeer.DumbSet) + is.SetAllowMultiReceive(true) + if n == 0 { + for _, h := range initialSet { + is.AddUnchecked(h) + } + } + require.False(t, hs[n].Synced()) + hs[n].Start() + } + + require.Eventually(t, func() bool { + for n, hsync := range hs { + // use a snapshot to avoid races + if !hsync.Synced() { + return false + } + os := hsync.Set().Copy(false) + for _, k := range handlers[n].committedItems() { + os.(*multipeer.DumbSet).AddUnchecked(k) + } + empty, err := os.Empty() + require.NoError(t, err) + if empty { + return false + } + k, err := os.Items().First() + require.NoError(t, err) + info, err := os.GetRangeInfo(k, k) + require.NoError(t, err) + if info.Count < numHashes { + return false + } + } + return true + }, 30*time.Second, 300*time.Millisecond) + + for n, hsync := range hs { + hsync.Stop() + os := hsync.Set().Copy(false) + for _, k := range handlers[n].committedItems() { + os.(*multipeer.DumbSet).AddUnchecked(k) + } + actualItems, err := os.Items().Collect() + require.NoError(t, err) + require.ElementsMatch(t, initialSet, actualItems) + } +} diff --git a/sync2/rangesync/p2p.go b/sync2/rangesync/p2p.go index f513dca63a..e3f70cba41 100644 --- a/sync2/rangesync/p2p.go +++ b/sync2/rangesync/p2p.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "sync/atomic" "github.com/spacemeshos/go-spacemesh/p2p" ) @@ -13,8 +14,8 @@ type PairwiseSetSyncer struct { name string opts []RangeSetReconcilerOption conduitOpts []ConduitOption - sent int - recv int + sent atomic.Int64 + recv atomic.Int64 } func NewPairwiseSetSyncer( @@ -32,8 +33,8 @@ func NewPairwiseSetSyncer( } func (pss *PairwiseSetSyncer) updateCounts(c *wireConduit) { - pss.sent += c.bytesSent() - pss.recv += c.bytesReceived() + pss.sent.Add(int64(c.bytesSent())) + pss.recv.Add(int64(c.bytesReceived())) } func (pss *PairwiseSetSyncer) Probe( @@ -126,9 +127,9 @@ func (pss *PairwiseSetSyncer) Register(d *Dispatcher, os OrderedSet) { } func (pss *PairwiseSetSyncer) Sent() int { - return pss.sent + return int(pss.sent.Load()) } func (pss *PairwiseSetSyncer) Received() int { - return pss.recv + return int(pss.recv.Load()) } From bb4316112ef82dfb87049c94b68fb0a9a85e67c8 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Wed, 23 Oct 2024 02:41:30 +0400 Subject: [PATCH 03/17] sync2: add sqlstore The `sqlstore` package provides simple sequence-based interface to the tables being synchronized. It is used by the FPTree data structure as the database layer, and doesn't do range fingerprinting by itself. `SyncedTable` and `SyncedTableSnapshot` provide methods that wrap the necessary SQL operations. `sql/expr` package was added to facilitate SQL generation. --- go.mod | 1 + go.sum | 4 + sql/database.go | 10 + sql/expr/expr.go | 212 ++++++++++++++ sql/expr/expr_test.go | 137 +++++++++ sync2/rangesync/combine_seqs.go | 196 +++++++++++++ sync2/rangesync/combine_seqs_test.go | 230 +++++++++++++++ sync2/sqlstore/dbseq.go | 236 ++++++++++++++++ sync2/sqlstore/dbseq_test.go | 260 +++++++++++++++++ sync2/sqlstore/export_test.go | 15 + sync2/sqlstore/interface.go | 21 ++ sync2/sqlstore/sqlidstore.go | 83 ++++++ sync2/sqlstore/sqlidstore_test.go | 82 ++++++ sync2/sqlstore/syncedtable.go | 284 +++++++++++++++++++ sync2/sqlstore/syncedtable_test.go | 404 +++++++++++++++++++++++++++ sync2/sqlstore/testdb.go | 46 +++ 16 files changed, 2221 insertions(+) create mode 100644 sql/expr/expr.go create mode 100644 sql/expr/expr_test.go create mode 100644 sync2/rangesync/combine_seqs.go create mode 100644 sync2/rangesync/combine_seqs_test.go create mode 100644 sync2/sqlstore/dbseq.go create mode 100644 sync2/sqlstore/dbseq_test.go create mode 100644 sync2/sqlstore/export_test.go create mode 100644 sync2/sqlstore/interface.go create mode 100644 sync2/sqlstore/sqlidstore.go create mode 100644 sync2/sqlstore/sqlidstore_test.go create mode 100644 sync2/sqlstore/syncedtable.go create mode 100644 sync2/sqlstore/syncedtable_test.go create mode 100644 sync2/sqlstore/testdb.go diff --git a/go.mod b/go.mod index 5f410e5b3f..d56deaaea2 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.60.0 github.com/quic-go/quic-go v0.48.1 + github.com/rqlite/sql v0.0.0-20240312185922-ffac88a740bd github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/seehuhn/mt19937 v1.0.0 diff --git a/go.sum b/go.sum index 15aca2b55a..594abaaba3 100644 --- a/go.sum +++ b/go.sum @@ -176,6 +176,8 @@ github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -577,6 +579,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rqlite/sql v0.0.0-20240312185922-ffac88a740bd h1:wW6BtayFoKaaDeIvXRE3SZVPOscSKlYD+X3bB749+zk= +github.com/rqlite/sql v0.0.0-20240312185922-ffac88a740bd/go.mod h1:ib9zVtNgRKiGuoMyUqqL5aNpk+r+++YlyiVIkclVqPg= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= diff --git a/sql/database.go b/sql/database.go index e527ef27a4..1630f07b57 100644 --- a/sql/database.go +++ b/sql/database.go @@ -11,6 +11,7 @@ import ( "strings" "sync" "sync/atomic" + "testing" "time" sqlite "github.com/go-llsqlite/crawshaw" @@ -236,6 +237,15 @@ func InMemory(opts ...Opt) *sqliteDatabase { return db } +// InMemoryTest returns an in-mem database for testing and ensures database is closed during `tb.Cleanup`. +func InMemoryTest(tb testing.TB, opts ...Opt) *sqliteDatabase { + // When using empty DB schema, we don't want to check for schema drift due to + // "PRAGMA user_version = 0;" in the initial schema retrieved from the DB. + db := InMemory(append(opts, WithNoCheckSchemaDrift())...) + tb.Cleanup(func() { db.Close() }) + return db +} + // Open database with options. // // Database is opened in WAL mode and pragma synchronous=normal. diff --git a/sql/expr/expr.go b/sql/expr/expr.go new file mode 100644 index 0000000000..adef890a59 --- /dev/null +++ b/sql/expr/expr.go @@ -0,0 +1,212 @@ +// Package expr proviedes a simple SQL expression parser and builder. +// It wraps the rqlite/sql package and provides a more convenient API that contains only +// what's needed for the go-spacemesh codebase. +package expr + +import ( + "strings" + + rsql "github.com/rqlite/sql" +) + +// SQL operations. +const ( + NE = rsql.NE // != + EQ = rsql.EQ // = + LE = rsql.LE // <= + LT = rsql.LT // < + GT = rsql.GT // > + GE = rsql.GE // >= + BITAND = rsql.BITAND // & + BITOR = rsql.BITOR // | + BITNOT = rsql.BITNOT // ! + LSHIFT = rsql.LSHIFT // << + RSHIFT = rsql.RSHIFT // >> + PLUS = rsql.PLUS // + + MINUS = rsql.MINUS // - + STAR = rsql.STAR // * + SLASH = rsql.SLASH // / + REM = rsql.REM // % + CONCAT = rsql.CONCAT // || + DOT = rsql.DOT // . + AND = rsql.AND + OR = rsql.OR + NOT = rsql.NOT +) + +// Expr represents a parsed SQL expression. +type Expr = rsql.Expr + +// Statement represents a parsed SQL statement. +type Statement = rsql.Statement + +// MustParse parses an SQL expression and panics if there's an error. +func MustParse(s string) rsql.Expr { + expr, err := rsql.ParseExprString(s) + if err != nil { + panic("error parsing SQL expression: " + err.Error()) + } + return expr +} + +// MustParseStatement parses an SQL statement and panics if there's an error. +func MustParseStatement(s string) rsql.Statement { + st, err := rsql.NewParser(strings.NewReader(s)).ParseStatement() + if err != nil { + panic("error parsing SQL statement: " + err.Error()) + } + return st +} + +// MaybeAnd joins together several SQL expressions with AND, ignoring any nil exprs. +// If no non-nil expressions are passed, nil is returned. +// If a single non-nil expression is passed, that single expression is returned. +// Otherwise, the expressions are joined together with ANDs: +// a AND b AND c AND d. +func MaybeAnd(exprs ...Expr) Expr { + var r Expr + for _, expr := range exprs { + switch { + case expr == nil: + case r == nil: + r = expr + default: + r = Op(r, AND, expr) + } + } + return r +} + +// Ident constructs SQL identifier expression for the identifier with the specified name. +func Ident(name string) *rsql.Ident { + return &rsql.Ident{Name: name} +} + +// Number constructs a number literal. +func Number(value string) *rsql.NumberLit { + return &rsql.NumberLit{Value: value} +} + +// TableSource constructs a Source clause for SELECT statement that corresponds to +// selecting from a single table with the specified name. +func TableSource(name string) rsql.Source { + return &rsql.QualifiedTableName{Name: Ident(name)} +} + +// Op constructs a binary expression such as x + y or x < y. +func Op(x Expr, op rsql.Token, y Expr) Expr { + return &rsql.BinaryExpr{ + X: x, + Op: op, + Y: y, + } +} + +// Bind constructs the unnamed bind expression (?). +func Bind() Expr { + return &rsql.BindExpr{Name: "?"} +} + +// Between constructs BETWEEN expression: x BETWEEN a AND b. +func Between(x, a, b Expr) Expr { + return Op(x, rsql.BETWEEN, &rsql.Range{X: a, Y: b}) +} + +// Call constructs a call expression with specified arguments such as max(x). +func Call(name string, args ...Expr) Expr { + return &rsql.Call{Name: Ident(name), Args: args} +} + +// CountStar returns a COUNT(*) expression. +func CountStar() Expr { + return &rsql.Call{Name: Ident("count"), Star: rsql.Pos{Offset: 1}} +} + +// Asc constructs an ascending ORDER BY term. +func Asc(expr Expr) *rsql.OrderingTerm { + return &rsql.OrderingTerm{X: expr} +} + +// Desc constructs a descedning ORDER BY term. +func Desc(expr Expr) *rsql.OrderingTerm { + return &rsql.OrderingTerm{X: expr, Desc: rsql.Pos{Offset: 1}} +} + +// SelectBuilder is used to construct a SELECT statement. +type SelectBuilder struct { + st *rsql.SelectStatement +} + +// Select returns a SELECT statement builder. +func Select(columns ...any) SelectBuilder { + sb := SelectBuilder{st: &rsql.SelectStatement{}} + return sb.Columns(columns...) +} + +// SelectBasedOn returns a SELECT statement builder based on the specified SELECT statement. +// The statement must be parseable, otherwise SelectBasedOn panics. +// The builder methods can be used to alter the statement. +func SelectBasedOn(st Statement) SelectBuilder { + st = rsql.CloneStatement(st) + return SelectBuilder{st: st.(*rsql.SelectStatement)} +} + +// Get returns the underlying SELECT statement. +func (sb SelectBuilder) Get() *rsql.SelectStatement { + return sb.st +} + +// String returns the underlying SELECT statement as a string. +func (sb SelectBuilder) String() string { + return sb.st.String() +} + +// Columns sets columns in the SELECT statement. +func (sb SelectBuilder) Columns(columns ...any) SelectBuilder { + sb.st.Columns = make([]*rsql.ResultColumn, len(columns)) + for n, column := range columns { + switch c := column.(type) { + case *rsql.ResultColumn: + sb.st.Columns[n] = c + case Expr: + sb.st.Columns[n] = &rsql.ResultColumn{Expr: c} + default: + panic("unexpected column type") + } + } + return sb +} + +// From adds FROM clause to the SELECT statement. +func (sb SelectBuilder) From(s rsql.Source) SelectBuilder { + sb.st.Source = s + return sb +} + +// From adds WHERE clause to the SELECT statement. +func (sb SelectBuilder) Where(s Expr) SelectBuilder { + sb.st.WhereExpr = s + return sb +} + +// From adds ORDER BY clause to the SELECT statement. +func (sb SelectBuilder) OrderBy(terms ...*rsql.OrderingTerm) SelectBuilder { + sb.st.OrderingTerms = terms + return sb +} + +// From adds LIMIT clause to the SELECT statement. +func (sb SelectBuilder) Limit(limit Expr) SelectBuilder { + sb.st.LimitExpr = limit + return sb +} + +// ColumnExpr returns nth column expression from the SELECT statement. +func ColumnExpr(st Statement, n int) Expr { + return st.(*rsql.SelectStatement).Columns[n].Expr +} + +// WhereExpr returns WHERE expression from the SELECT statement. +func WhereExpr(st Statement) Expr { + return st.(*rsql.SelectStatement).WhereExpr +} diff --git a/sql/expr/expr_test.go b/sql/expr/expr_test.go new file mode 100644 index 0000000000..8a87bc8395 --- /dev/null +++ b/sql/expr/expr_test.go @@ -0,0 +1,137 @@ +package expr + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExpr(t *testing.T) { + for _, tc := range []struct { + Expr Expr + Expected string + }{ + { + Expr: MustParse("a = ? OR x < 10"), + Expected: `"a" = ? OR "x" < 10`, + }, + { + Expr: Number("1"), + Expected: `1`, + }, + { + Expr: CountStar(), + Expected: `count(*)`, + }, + { + Expr: Op(Ident("x"), EQ, Ident("y")), + Expected: `"x" = "y"`, + }, + { + Expr: MaybeAnd(Op(Ident("x"), EQ, Ident("y"))), + Expected: `"x" = "y"`, + }, + { + Expr: MaybeAnd(Op(Ident("x"), EQ, Ident("y")), nil, nil), + Expected: `"x" = "y"`, + }, + { + Expr: MaybeAnd(Op(Ident("x"), EQ, Ident("y")), + Op(Ident("a"), EQ, Bind())), + Expected: `"x" = "y" AND "a" = ?`, + }, + { + Expr: MaybeAnd(Op(Ident("x"), EQ, Ident("y")), + nil, + Op(Ident("a"), EQ, Bind())), + Expected: `"x" = "y" AND "a" = ?`, + }, + { + Expr: MaybeAnd(), + Expected: "", + }, + { + Expr: Between(Ident("x"), Ident("y"), Bind()), + Expected: `"x" BETWEEN "y" AND ?`, + }, + { + Expr: Call("max", Ident("x")), + Expected: `max("x")`, + }, + { + Expr: MustParse("a.id"), + Expected: `"a"."id"`, + }, + } { + if tc.Expected == "" { + require.Nil(t, tc.Expr) + } else { + require.Equal(t, tc.Expected, tc.Expr.String()) + require.Equal(t, tc.Expected, MustParse(tc.Expected).String()) + } + } +} + +func TestStatement(t *testing.T) { + for _, tc := range []struct { + Statement SelectBuilder + Expected string + Columns []string + }{ + { + Statement: Select(Number("1")), + Expected: `SELECT 1`, + Columns: []string{"1"}, + }, + { + Statement: Select(Call("max", Ident("n"))).From(TableSource("mytable")), + Expected: `SELECT max("n") FROM "mytable"`, + Columns: []string{`max("n")`}, + }, + { + Statement: Select(Ident("id"), Ident("n")). + From(TableSource("mytable")). + Where(Op(Ident("n"), GE, Bind())). + OrderBy(Asc(Ident("n"))). + Limit(Bind()), + Expected: `SELECT "id", "n" FROM "mytable" WHERE "n" >= ? ORDER BY "n" LIMIT ?`, + Columns: []string{`"id"`, `"n"`}, + }, + { + Statement: Select(Ident("id")). + From(TableSource("mytable")). + OrderBy(Desc(Ident("id"))). + Limit(Number("10")), + Expected: `SELECT "id" FROM "mytable" ORDER BY "id" DESC LIMIT 10`, + Columns: []string{`"id"`}, + }, + { + Statement: Select(CountStar()).From(TableSource("mytable")), + Expected: `SELECT count(*) FROM "mytable"`, + Columns: []string{`count(*)`}, + }, + { + Statement: SelectBasedOn( + MustParseStatement("select a.id from a left join b on a.x = b.x")). + Where(Op(Ident("id"), EQ, Bind())), + Expected: `SELECT "a"."id" FROM "a" LEFT JOIN "b" ON "a"."x" = "b"."x" WHERE "id" = ?`, + Columns: []string{`"a"."id"`}, + }, + { + Statement: SelectBasedOn( + MustParseStatement("select a.id from a inner join b on a.x = b.x")). + Columns(CountStar()). + Where(Op(Ident("id"), EQ, Bind())), + Expected: `SELECT count(*) FROM "a" INNER JOIN "b" ON "a"."x" = "b"."x" WHERE "id" = ?`, + Columns: []string{`count(*)`}, + }, + } { + require.Equal(t, tc.Expected, tc.Statement.String()) + st := tc.Statement.Get() + require.Equal(t, tc.Expected, st.String()) + for n, col := range tc.Columns { + require.Equal(t, col, ColumnExpr(st, n).String()) + } + require.Equal(t, tc.Expected, MustParseStatement(tc.Expected).String()) + } +} diff --git a/sync2/rangesync/combine_seqs.go b/sync2/rangesync/combine_seqs.go new file mode 100644 index 0000000000..8c924fd8e3 --- /dev/null +++ b/sync2/rangesync/combine_seqs.go @@ -0,0 +1,196 @@ +package rangesync + +import ( + "iter" + "slices" +) + +type generator struct { + nextFn func() (KeyBytes, bool) + stop func() + k KeyBytes + error SeqErrorFunc + done bool +} + +func gen(sr SeqResult) *generator { + g := &generator{error: sr.Error} + g.nextFn, g.stop = iter.Pull(iter.Seq[KeyBytes](sr.Seq)) + return g +} + +func (g *generator) next() (k KeyBytes, ok bool) { + if g.done { + return nil, false + } + if g.k != nil { + k = g.k + g.k = nil + return k, true + } + return g.nextFn() +} + +func (g *generator) peek() (k KeyBytes, ok bool) { + if !g.done && g.k == nil { + g.k, ok = g.nextFn() + g.done = !ok + } + if g.done { + return nil, false + } + return g.k, true +} + +type combinedSeq struct { + gens []*generator + wrapped []*generator +} + +// CombineSeqs combines multiple ordered sequences from SeqResults into one, returning the +// smallest current key among all iterators at each step. +// startingPoint is used to check if an iterator has wrapped around. If an iterator yields +// a value below startingPoint, it is considered to have wrapped around. +func CombineSeqs(startingPoint KeyBytes, srs ...SeqResult) SeqResult { + var err error + return SeqResult{ + Seq: func(yield func(KeyBytes) bool) { + var c combinedSeq + // We clean up even if c.begin() returned an error so that we don't leak + // any pull iterators that are created before c.begin() failed + defer c.end() + // In case if c.begin() succeeds, the error is reset. If yield + // calls SeqResult's Error function, it will get nil until the + // iteration is finished. + if err = c.begin(startingPoint, srs); err != nil { + return + } + err = c.iterate(yield) + }, + Error: func() error { + return err + }, + } +} + +func (c *combinedSeq) begin(startingPoint KeyBytes, srs []SeqResult) error { + for _, sr := range srs { + g := gen(sr) + k, ok := g.peek() + if !ok { + continue + } + if err := g.error(); err != nil { + return err + } + if startingPoint != nil && k.Compare(startingPoint) < 0 { + c.wrapped = append(c.wrapped, g) + } else { + c.gens = append(c.gens, g) + } + } + if len(c.gens) == 0 { + // all iterators wrapped around + c.gens = c.wrapped + c.wrapped = nil + } + return nil +} + +func (c *combinedSeq) end() { + for _, g := range c.gens { + g.stop() + } + for _, g := range c.wrapped { + g.stop() + } +} + +func (c *combinedSeq) aheadGen() (ahead *generator, aheadIdx int, err error) { + // remove any exhausted generators + j := 0 + for i := range c.gens { + _, ok := c.gens[i].peek() + if ok { + c.gens[j] = c.gens[i] + j++ + } else if err = c.gens[i].error(); err != nil { + return nil, 0, err + } + } + c.gens = c.gens[:j] + // if all the generators have wrapped around, move the wrapped generators + if len(c.gens) == 0 { + if len(c.wrapped) == 0 { + return nil, 0, nil + } + c.gens = c.wrapped + c.wrapped = nil + } + ahead = c.gens[0] + aheadIdx = 0 + aK, _ := ahead.peek() + if err := ahead.error(); err != nil { + return nil, 0, err + } + for i := 1; i < len(c.gens); i++ { + curK, ok := c.gens[i].peek() + if !ok { + // If not all of the generators have wrapped around, then we + // already did a successful peek() on this generator above, so it + // should not be exhausted here. + // If all of the generators have wrapped around, then we have + // moved to the wrapped generators, but the generators may only + // end up in wrapped list after a successful peek(), too. + // So if we get here, then combinedSeq code is broken. + panic("BUG: unexpected exhausted generator") + } + if curK != nil { + if curK.Compare(aK) < 0 { + ahead = c.gens[i] + aheadIdx = i + aK = curK + } + } + } + return ahead, aheadIdx, nil +} + +func (c *combinedSeq) iterate(yield func(KeyBytes) bool) error { + for { + g, idx, err := c.aheadGen() + if err != nil { + return err + } + if g == nil { + return nil + } + k, ok := g.next() + if !ok { + if err := g.error(); err != nil { + return err + } + c.gens = slices.Delete(c.gens, idx, idx+1) + continue + } + if !yield(k) { + return nil + } + newK, ok := g.peek() + if !ok { + if err := g.error(); err != nil { + return err + } + // if this iterator is exhausted, it'll be removed by the + // next aheadGen call + continue + } + if k.Compare(newK) >= 0 { + // the iterator has wrapped around, move it to the wrapped + // list which will be used after all the iterators have + // wrapped around + c.wrapped = append(c.wrapped, g) + c.gens = slices.Delete(c.gens, idx, idx+1) + } + } +} diff --git a/sync2/rangesync/combine_seqs_test.go b/sync2/rangesync/combine_seqs_test.go new file mode 100644 index 0000000000..ec1fbc8849 --- /dev/null +++ b/sync2/rangesync/combine_seqs_test.go @@ -0,0 +1,230 @@ +package rangesync + +import ( + "errors" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +var seqTestErr = errors.New("test error") + +type fakeSeqItem struct { + k string + err error + stop bool +} + +func mkFakeSeqItem(s string) fakeSeqItem { + switch s { + case "!": + return fakeSeqItem{err: seqTestErr} + case "$": + return fakeSeqItem{stop: true} + default: + return fakeSeqItem{k: s} + } +} + +type fakeSeq []fakeSeqItem + +func mkFakeSeq(s string) fakeSeq { + seq := make(fakeSeq, len(s)) + for n, c := range s { + seq[n] = mkFakeSeqItem(string(c)) + } + return seq +} + +func (seq fakeSeq) items(startIdx int) SeqResult { + if startIdx > len(seq) { + panic("bad startIdx") + } + var err error + return SeqResult{ + Seq: func(yield func(KeyBytes) bool) { + err = nil + if len(seq) == 0 { + return + } + n := startIdx + for { + if n == len(seq) { + n = 0 + } + item := seq[n] + if item.err != nil { + err = item.err + return + } + if item.stop || !yield(KeyBytes(item.k)) { + return + } + n++ + } + }, + Error: func() error { + return err + }, + } +} + +func seqToStr(t *testing.T, sr SeqResult) string { + var sb strings.Builder + var firstK KeyBytes + wrap := 0 + var s string + for k := range sr.Seq { + require.NoError(t, sr.Error()) + if wrap != 0 { + // after wraparound, make sure the sequence is repeated + if k.Compare(firstK) == 0 { + // arrived to the element for the second time + return s + } + require.Equal(t, s[wrap], k[0]) + wrap++ + continue + } + require.NotNil(t, k) + if firstK == nil { + firstK = k + } else if k.Compare(firstK) == 0 { + s = sb.String() // wraparound + wrap = 1 + continue + } + sb.Write(k) + } + if err := sr.Error(); err != nil { + require.Equal(t, seqTestErr, err) + sb.WriteString("!") // error + return sb.String() + } + return sb.String() + "$" // stop +} + +func TestCombineSeqs(t *testing.T) { + for _, tc := range []struct { + // In each seq, $ means the end of sequence (lack of $ means wraparound), + // and ! means an error. + seqs []string + indices []int + result string + startingPoint string + }{ + { + seqs: []string{"abcd"}, + indices: []int{0}, + result: "abcd", + startingPoint: "a", + }, + { + seqs: []string{"abcd"}, + indices: []int{0}, + result: "abcd", + startingPoint: "c", + }, + { + seqs: []string{"abcd"}, + indices: []int{2}, + result: "cdab", + startingPoint: "c", + }, + { + seqs: []string{"abcd$"}, + indices: []int{0}, + result: "abcd$", + startingPoint: "a", + }, + { + seqs: []string{"abcd!"}, + indices: []int{0}, + result: "abcd!", + startingPoint: "a", + }, + { + seqs: []string{"abcd", "efgh"}, + indices: []int{0, 0}, + result: "abcdefgh", + startingPoint: "a", + }, + { + seqs: []string{"aceg", "bdfh"}, + indices: []int{0, 0}, + result: "abcdefgh", + startingPoint: "a", + }, + { + seqs: []string{"abcd$", "efgh$"}, + indices: []int{0, 0}, + result: "abcdefgh$", + startingPoint: "a", + }, + { + seqs: []string{"aceg$", "bdfh$"}, + indices: []int{0, 0}, + result: "abcdefgh$", + startingPoint: "a", + }, + { + seqs: []string{"abcd!", "efgh!"}, + indices: []int{0, 0}, + result: "abcd!", + startingPoint: "a", + }, + { + seqs: []string{"aceg!", "bdfh!"}, + indices: []int{0, 0}, + result: "abcdefg!", + startingPoint: "a", + }, + { + // wraparound: + // "ac"+"bdefgh" + // abcdefgh ==> + // defghabc + // starting point is d. + // Each sequence must either start after the starting point, or + // all of its elements are considered to be below the starting + // point. "ac" is considered to be wrapped around initially + seqs: []string{"ac", "bdefgh"}, + indices: []int{0, 1}, + result: "defghabc", + startingPoint: "d", + }, + { + seqs: []string{"bc", "ae"}, + indices: []int{0, 1}, + result: "eabc", + startingPoint: "d", + }, + { + seqs: []string{"ac", "bfg", "deh"}, + indices: []int{0, 0, 0}, + result: "abcdefgh", + startingPoint: "a", + }, + { + seqs: []string{"abdefgh", "c"}, + indices: []int{0, 0}, + result: "abcdefgh", + startingPoint: "a", + }, + } { + t.Run("", func(t *testing.T) { + var seqs []SeqResult + for n, s := range tc.seqs { + seqs = append(seqs, mkFakeSeq(s).items(tc.indices[n])) + } + startingPoint := KeyBytes(tc.startingPoint) + combined := CombineSeqs(startingPoint, seqs...) + for range 3 { // make sure the sequence is reusable + require.Equal(t, tc.result, seqToStr(t, combined), + "combine %v (indices %v) starting with %s", + tc.seqs, tc.indices, tc.startingPoint) + } + }) + } +} diff --git a/sync2/sqlstore/dbseq.go b/sync2/sqlstore/dbseq.go new file mode 100644 index 0000000000..f2d1c82dff --- /dev/null +++ b/sync2/sqlstore/dbseq.go @@ -0,0 +1,236 @@ +package sqlstore + +import ( + "errors" + "slices" + + "github.com/hashicorp/golang-lru/v2/simplelru" + + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +// dbIDKey is a key for the LRU cache of ID chunks. +type dbIDKey struct { + //nolint:unused + id string + //nolint:unused + chunkSize int +} + +// LRU cache for ID chunks. +type lru = simplelru.LRU[dbIDKey, []rangesync.KeyBytes] + +const lruCacheSize = 1024 * 1024 + +// newLRU creates a new LRU cache for ID chunks. +func newLRU() *lru { + cache, err := simplelru.NewLRU[dbIDKey, []rangesync.KeyBytes](lruCacheSize, nil) + if err != nil { + panic("BUG: failed to create LRU cache: " + err.Error()) + } + return cache +} + +// dbSeq represents a sequence of IDs from a database table. +type dbSeq struct { + // database + db sql.Executor + // starting point + from rangesync.KeyBytes + // table snapshot to use + sts *SyncedTableSnapshot + // currently used chunk size + chunkSize int + // timestamp used to fetch recent IDs + // (nanoseconds since epoch, 0 if not in the "recent" mode) + ts int64 + // maximum value for chunkSize + maxChunkSize int + // current chunk of items + chunk []rangesync.KeyBytes + // position within the current chunk + pos int + // lentgh of each key in bytes + keyLen int + // true if there is only a single chunk in the sequence. + // It is set after loading the initial chunk and finding that it's the only one. + singleChunk bool + // LRU cache for ID chunks + cache *lru +} + +// idsFromTable iterates over the id field values in a database table. +func idsFromTable( + db sql.Executor, + sts *SyncedTableSnapshot, + from rangesync.KeyBytes, + ts int64, + chunkSize int, + maxChunkSize int, + lru *lru, +) rangesync.SeqResult { + if from == nil { + panic("BUG: makeDBIterator: nil from") + } + if maxChunkSize <= 0 { + panic("BUG: makeDBIterator: chunkSize must be > 0") + } + if chunkSize <= 0 { + chunkSize = 1 + } else if chunkSize > maxChunkSize { + chunkSize = maxChunkSize + } + var err error + return rangesync.SeqResult{ + Seq: func(yield func(k rangesync.KeyBytes) bool) { + s := &dbSeq{ + db: db, + from: from.Clone(), + sts: sts, + chunkSize: chunkSize, + ts: ts, + maxChunkSize: maxChunkSize, + keyLen: len(from), + chunk: make([]rangesync.KeyBytes, maxChunkSize), + singleChunk: false, + cache: lru, + } + if err = s.load(); err != nil { + return + } + err = s.iterate(yield) + }, + Error: func() error { + return err + }, + } +} + +// loadCached loads a chunk of IDs from the LRU cache, +// if possible. +func (s *dbSeq) loadCached(key dbIDKey) (bool, int) { + if s.cache == nil { + return false, 0 + } + chunk, ok := s.cache.Get(key) + if !ok { + return false, 0 + } + + for n, id := range s.chunk[:len(chunk)] { + if id == nil { + id = make([]byte, s.keyLen) + s.chunk[n] = id + } + copy(id, chunk[n]) + } + return true, len(chunk) +} + +// load makes sure the current chunk is loaded. +func (s *dbSeq) load() error { + s.pos = 0 + if s.singleChunk { + // we have a single-chunk DB sequence, don't need to reload, + // just wrap around + return nil + } + + n := 0 + // if the chunk size was reduced due to a short chunk before wraparound, we need + // to extend it back + if cap(s.chunk) < s.chunkSize { + s.chunk = make([]rangesync.KeyBytes, s.chunkSize) + } else { + s.chunk = s.chunk[:s.chunkSize] + } + key := dbIDKey{string(s.from), s.chunkSize} + var ierr, err error + found, n := s.loadCached(key) + if !found { + dec := func(stmt *sql.Statement) bool { + if n >= len(s.chunk) { + ierr = errors.New("too many rows") + return false + } + // we reuse existing slices when possible for retrieving new IDs + id := s.chunk[n] + if id == nil { + id = make([]byte, s.keyLen) + s.chunk[n] = id + } + stmt.ColumnBytes(0, id) + n++ + return true + } + if s.ts <= 0 { + err = s.sts.LoadRange(s.db, s.from, s.chunkSize, dec) + } else { + err = s.sts.LoadRecent(s.db, s.from, s.chunkSize, s.ts, dec) + } + if err == nil && ierr == nil && s.cache != nil { + cached := make([]rangesync.KeyBytes, n) + for n, id := range s.chunk[:n] { + cached[n] = slices.Clone(id) + } + s.cache.Add(key, cached) + } + } + fromZero := s.from.IsZero() + s.chunkSize = min(s.chunkSize*2, s.maxChunkSize) + switch { + case err != nil || ierr != nil: + return errors.Join(ierr, err) + case n == 0: + // empty chunk + if fromZero { + // already wrapped around or started from 0, + // the set is empty + s.chunk = nil + return nil + } + // wrap around + s.from.Zero() + return s.load() + case n < len(s.chunk): + // short chunk means there are no more items after it, + // start the next chunk from 0 + s.from.Zero() + s.chunk = s.chunk[:n] + // wrapping around on an incomplete chunk that started + // from 0 means we have just a single chunk + s.singleChunk = fromZero + default: + // use last item incremented by 1 as the start of the next chunk + copy(s.from, s.chunk[n-1]) + // inc may wrap around if it's 0xffff...fff, but it's fine + if s.from.Inc() { + // if we wrapped around and the current chunk started from 0, + // we have just a single chunk + s.singleChunk = fromZero + } + } + return nil +} + +// iterate iterates over the table rows. +func (s *dbSeq) iterate(yield func(k rangesync.KeyBytes) bool) error { + if len(s.chunk) == 0 { + return nil + } + for { + if s.pos >= len(s.chunk) { + panic("BUG: bad dbSeq position") + } + if !yield(slices.Clone(s.chunk[s.pos])) { + return nil + } + s.pos++ + if s.pos >= len(s.chunk) { + if err := s.load(); err != nil { + return err + } + } + } +} diff --git a/sync2/sqlstore/dbseq_test.go b/sync2/sqlstore/dbseq_test.go new file mode 100644 index 0000000000..87720007d6 --- /dev/null +++ b/sync2/sqlstore/dbseq_test.go @@ -0,0 +1,260 @@ +package sqlstore_test + +import ( + "encoding/hex" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" + "github.com/spacemeshos/go-spacemesh/sync2/sqlstore" +) + +func TestDBRangeIterator(t *testing.T) { + for _, tc := range []struct { + items []rangesync.KeyBytes + from rangesync.KeyBytes + fromN int + }{ + { + items: nil, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x00}, + }, + { + items: nil, + from: rangesync.KeyBytes{0x80, 0x00, 0x00, 0x00}, + }, + { + items: nil, + from: rangesync.KeyBytes{0xff, 0xff, 0xff, 0xff}, + }, + { + items: []rangesync.KeyBytes{ + {0x00, 0x00, 0x00, 0x00}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x00}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + {0x00, 0x00, 0x00, 0x00}, + }, + from: rangesync.KeyBytes{0x01, 0x00, 0x00, 0x00}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + {0x00, 0x00, 0x00, 0x00}, + }, + from: rangesync.KeyBytes{0xff, 0xff, 0xff, 0xff}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + {0x01, 0x02, 0x03, 0x04}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x00}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + {0x01, 0x02, 0x03, 0x04}, + }, + from: rangesync.KeyBytes{0x01, 0x00, 0x00, 0x00}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + {0x01, 0x02, 0x03, 0x04}, + }, + from: rangesync.KeyBytes{0xff, 0xff, 0xff, 0xff}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + {0xff, 0xff, 0xff, 0xff}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x00}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + {0xff, 0xff, 0xff, 0xff}, + }, + from: rangesync.KeyBytes{0x01, 0x00, 0x00, 0x00}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + {0xff, 0xff, 0xff, 0xff}, + }, + from: rangesync.KeyBytes{0xff, 0xff, 0xff, 0xff}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x00}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x01}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x02}, + fromN: 1, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x03}, + fromN: 1, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x05}, + fromN: 2, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x00, 0x07}, + fromN: 3, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + }, + from: rangesync.KeyBytes{0xff, 0xff, 0xff, 0xff}, + fromN: 0, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + 4: {0x00, 0x00, 0x01, 0x00}, + 5: {0x00, 0x00, 0x03, 0x00}, + 6: {0x00, 0x01, 0x00, 0x00}, + 7: {0x00, 0x05, 0x00, 0x00}, + 8: {0x03, 0x05, 0x00, 0x00}, + 9: {0x09, 0x05, 0x00, 0x00}, + 10: {0x0a, 0x05, 0x00, 0x00}, + 11: {0xff, 0xff, 0xff, 0xff}, + }, + from: rangesync.KeyBytes{0x00, 0x00, 0x03, 0x01}, + fromN: 6, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + 4: {0x00, 0x00, 0x01, 0x00}, + 5: {0x00, 0x00, 0x03, 0x00}, + 6: {0x00, 0x01, 0x00, 0x00}, + 7: {0x00, 0x05, 0x00, 0x00}, + 8: {0x03, 0x05, 0x00, 0x00}, + 9: {0x09, 0x05, 0x00, 0x00}, + 10: {0x0a, 0x05, 0x00, 0x00}, + 11: {0xff, 0xff, 0xff, 0xff}, + }, + from: rangesync.KeyBytes{0x00, 0x01, 0x00, 0x00}, + fromN: 6, + }, + { + items: []rangesync.KeyBytes{ + 0: {0x00, 0x00, 0x00, 0x01}, + 1: {0x00, 0x00, 0x00, 0x03}, + 2: {0x00, 0x00, 0x00, 0x05}, + 3: {0x00, 0x00, 0x00, 0x07}, + 4: {0x00, 0x00, 0x01, 0x00}, + 5: {0x00, 0x00, 0x03, 0x00}, + 6: {0x00, 0x01, 0x00, 0x00}, + 7: {0x00, 0x05, 0x00, 0x00}, + 8: {0x03, 0x05, 0x00, 0x00}, + 9: {0x09, 0x05, 0x00, 0x00}, + 10: {0x0a, 0x05, 0x00, 0x00}, + 11: {0xff, 0xff, 0xff, 0xff}, + }, + from: rangesync.KeyBytes{0xff, 0xff, 0xff, 0xff}, + fromN: 11, + }, + } { + t.Run("", func(t *testing.T) { + db := sqlstore.CreateDB(t, 4) + sqlstore.InsertDBItems(t, db, tc.items) + cache := sqlstore.NewLRU() + st := &sqlstore.SyncedTable{ + TableName: "foo", + IDColumn: "id", + } + sts, err := st.Snapshot(db) + require.NoError(t, err) + for startChunkSize := 1; startChunkSize < 12; startChunkSize++ { + for maxChunkSize := 1; maxChunkSize < 12; maxChunkSize++ { + sr := sqlstore.IDSFromTable(db, sts, tc.from, -1, + startChunkSize, maxChunkSize, cache) + // when there are no items, errEmptySet is returned + for range 3 { // make sure the sequence is reusable + var collected []rangesync.KeyBytes + var firstK rangesync.KeyBytes + for k := range sr.Seq { + if firstK == nil { + firstK = k + } else if k.Compare(firstK) == 0 { + break + } + collected = append(collected, k) + require.NoError(t, err) + } + require.NoError(t, sr.Error()) + expected := slices.Concat( + tc.items[tc.fromN:], tc.items[:tc.fromN]) + require.Equal( + t, expected, collected, + "count=%d from=%s maxChunkSize=%d", + len(tc.items), hex.EncodeToString(tc.from), + maxChunkSize) + } + } + } + }) + } +} diff --git a/sync2/sqlstore/export_test.go b/sync2/sqlstore/export_test.go new file mode 100644 index 0000000000..6c0c5c9167 --- /dev/null +++ b/sync2/sqlstore/export_test.go @@ -0,0 +1,15 @@ +package sqlstore + +import "github.com/spacemeshos/go-spacemesh/sql/expr" + +var ( + NewLRU = newLRU + IDSFromTable = idsFromTable +) + +func (st *SyncedTable) GenSelectAll() expr.Statement { return st.genSelectAll() } +func (st *SyncedTable) GenCount() expr.Statement { return st.genCount() } +func (st *SyncedTable) GenSelectMaxRowID() expr.Statement { return st.genSelectMaxRowID() } +func (st *SyncedTable) GenSelectRange() expr.Statement { return st.genSelectRange() } +func (st *SyncedTable) GenRecentCount() expr.Statement { return st.genRecentCount() } +func (st *SyncedTable) GenSelectRecent() expr.Statement { return st.genSelectRecent() } diff --git a/sync2/sqlstore/interface.go b/sync2/sqlstore/interface.go new file mode 100644 index 0000000000..ecb38e93e1 --- /dev/null +++ b/sync2/sqlstore/interface.go @@ -0,0 +1,21 @@ +package sqlstore + +import ( + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +// IDStore represents a store of IDs (keys). +type IDStore interface { + // Clone makes a copy of the store. + // It is expected to be an O(1) operation. + Clone() IDStore + // Release releases the resources associated with the store. + Release() + // RegisterKey registers the key with the store. + RegisterKey(k rangesync.KeyBytes) error + // All returns all keys in the store. + All() rangesync.SeqResult + // From returns all keys in the store starting from the given key. + // sizeHint is a hint for the expected number of keys to be returned. + From(from rangesync.KeyBytes, sizeHint int) rangesync.SeqResult +} diff --git a/sync2/sqlstore/sqlidstore.go b/sync2/sqlstore/sqlidstore.go new file mode 100644 index 0000000000..bccdb97d54 --- /dev/null +++ b/sync2/sqlstore/sqlidstore.go @@ -0,0 +1,83 @@ +package sqlstore + +import ( + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +// max chunk size to use for dbSeq. +const sqlMaxChunkSize = 1024 + +// SQLIDStore is an implementation of IDStore that is based on a database table snapshot. +type SQLIDStore struct { + db sql.Executor + sts *SyncedTableSnapshot + keyLen int + cache *lru +} + +var _ IDStore = &SQLIDStore{} + +// NewSQLIDStore creates a new SQLIDStore. +func NewSQLIDStore(db sql.Executor, sts *SyncedTableSnapshot, keyLen int) *SQLIDStore { + return &SQLIDStore{ + db: db, + sts: sts, + keyLen: keyLen, + cache: newLRU(), + } +} + +// Clone creates a new SQLIDStore that shares the same database connection and table snapshot. +// Implements IDStore. +func (s *SQLIDStore) Clone() IDStore { + return NewSQLIDStore(s.db, s.sts, s.keyLen) +} + +// RegisterKey is a no-op for SQLIDStore, as the underlying table is never immediately +// updated upon receiving new items. +// Implements IDStore. +func (s *SQLIDStore) RegisterKey(k rangesync.KeyBytes) error { + // should be registered by the handler code + return nil +} + +// All returns all IDs in the store. +// Implements IDStore. +func (s *SQLIDStore) All() rangesync.SeqResult { + return s.From(make(rangesync.KeyBytes, s.keyLen), 1) +} + +// From returns IDs in the store starting from the given key. +// Implements IDStore. +func (s *SQLIDStore) From(from rangesync.KeyBytes, sizeHint int) rangesync.SeqResult { + if len(from) != s.keyLen { + panic("BUG: invalid key length") + } + return idsFromTable(s.db, s.sts, from, -1, sizeHint, sqlMaxChunkSize, s.cache) +} + +// Since returns IDs in the store starting from the given key and timestamp. +func (s *SQLIDStore) Since(from rangesync.KeyBytes, since int64) (rangesync.SeqResult, int) { + if len(from) != s.keyLen { + panic("BUG: invalid key length") + } + count, err := s.sts.LoadRecentCount(s.db, since) + if err != nil { + return rangesync.ErrorSeqResult(err), 0 + } + if count == 0 { + return rangesync.EmptySeqResult(), 0 + } + return idsFromTable(s.db, s.sts, from, since, 1, sqlMaxChunkSize, nil), count +} + +// Sets the table snapshot to use for the store. +func (s *SQLIDStore) SetSnapshot(sts *SyncedTableSnapshot) { + s.sts = sts + s.cache.Purge() +} + +// Release is a no-op for SQLIDStore. +// Implements IDStore. +func (s *SQLIDStore) Release() {} diff --git a/sync2/sqlstore/sqlidstore_test.go b/sync2/sqlstore/sqlidstore_test.go new file mode 100644 index 0000000000..b8e75fb6e2 --- /dev/null +++ b/sync2/sqlstore/sqlidstore_test.go @@ -0,0 +1,82 @@ +package sqlstore_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" + "github.com/spacemeshos/go-spacemesh/sync2/sqlstore" +) + +func TestSQLIdStore(t *testing.T) { + const keyLen = 12 + db := sql.InMemoryTest(t) + _, err := db.Exec( + fmt.Sprintf("create table foo(id char(%d) not null primary key, received int)", keyLen), + nil, nil) + require.NoError(t, err) + for _, row := range []struct { + id rangesync.KeyBytes + ts int64 + }{ + { + id: rangesync.KeyBytes{0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, + ts: 100, + }, + { + id: rangesync.KeyBytes{0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0}, + ts: 200, + }, + { + id: rangesync.KeyBytes{0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0}, + ts: 300, + }, + { + id: rangesync.KeyBytes{0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0}, + ts: 400, + }, + } { + _, err := db.Exec( + "insert into foo (id, received) values (?, ?)", + func(stmt *sql.Statement) { + stmt.BindBytes(1, row.id) + stmt.BindInt64(2, row.ts) + }, nil) + require.NoError(t, err) + } + st := sqlstore.SyncedTable{ + TableName: "foo", + IDColumn: "id", + TimestampColumn: "received", + } + sts, err := st.Snapshot(db) + require.NoError(t, err) + + store := sqlstore.NewSQLIDStore(db, sts, keyLen) + actualIDs, err := store.From(rangesync.KeyBytes{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 5).FirstN(5) + require.NoError(t, err) + require.Equal(t, []rangesync.KeyBytes{ + {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, // wrapped around + }, actualIDs) + + actualIDs1, err := store.All().FirstN(5) + require.NoError(t, err) + require.Equal(t, actualIDs, actualIDs1) + + sr, count := store.Since(rangesync.KeyBytes{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 300) + require.Equal(t, 2, count) + actualIDs, err = sr.FirstN(3) + require.NoError(t, err) + require.Equal(t, []rangesync.KeyBytes{ + {0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0}, // wrapped around + }, actualIDs) +} diff --git a/sync2/sqlstore/syncedtable.go b/sync2/sqlstore/syncedtable.go new file mode 100644 index 0000000000..04b6a6cefc --- /dev/null +++ b/sync2/sqlstore/syncedtable.go @@ -0,0 +1,284 @@ +package sqlstore + +import ( + "errors" + "fmt" + + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/expr" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +// Binder is a function that binds filter expression parameters to a SQL statement. +type Binder func(s *sql.Statement) + +// SyncedTable represents a table that can be used with SQLIDStore. +type SyncedTable struct { + // The name of the table. + TableName string + // The name of the ID column. + IDColumn string + // The name of the timestamp column. + TimestampColumn string + // The filter expression. + Filter expr.Expr + // The binder function for the bind parameters appearing in the filter expression. + Binder Binder +} + +// genSelectMaxRowID generates a SELECT statement that returns the maximum rowid in the +// table. +func (st *SyncedTable) genSelectMaxRowID() expr.Statement { + return expr.Select(expr.Call("max", expr.Ident("rowid"))). + From(expr.TableSource(st.TableName)). + Get() +} + +// rowIDCutoff returns an expression that represents a rowid cutoff, that is, limits the +// rowid to be less than or equal to a bind parameter. +func (st *SyncedTable) rowIDCutoff() expr.Expr { + return expr.Op(expr.Ident("rowid"), expr.LE, expr.Bind()) +} + +// timestampCutoff returns an expression that represents a timestamp cutoff, that is, limits the +// timestamp to be greater than or equal to a bind parameter. +func (st *SyncedTable) timestampCutoff() expr.Expr { + return expr.Op(expr.Ident(st.TimestampColumn), expr.GE, expr.Bind()) +} + +// genSelectAll generates a SELECT statement that returns all the rows in the table +// satisfying the filter expression and the rowid cutoff. +func (st *SyncedTable) genSelectAll() expr.Statement { + return expr.Select(expr.Ident(st.IDColumn)). + From(expr.TableSource(st.TableName)). + Where(expr.MaybeAnd(st.Filter, st.rowIDCutoff())). + Get() +} + +// genCount generates a SELECT statement that returns the number of rows in the table +// satisfying the filter expression and the rowid cutoff. +func (st *SyncedTable) genCount() expr.Statement { + return expr.Select(expr.Call("count", expr.Ident(st.IDColumn))). + From(expr.TableSource(st.TableName)). + Where(expr.MaybeAnd(st.Filter, st.rowIDCutoff())). + Get() +} + +// genSelectAllSinceSnapshot generates a SELECT statement that returns all the rows in the +// table satisfying the filter expression that have rowid between the specified min and +// max parameter values, inclusive. +func (st *SyncedTable) genSelectAllSinceSnapshot() expr.Statement { + return expr.Select(expr.Ident(st.IDColumn)). + From(expr.TableSource(st.TableName)). + Where(expr.MaybeAnd( + st.Filter, + expr.Between(expr.Ident("rowid"), expr.Bind(), expr.Bind()))). + Get() +} + +// genSelectRange generates a SELECT statement that returns the rows in the table +// satisfying the filter expression, the rowid cutoff and the specified ID range. +func (st *SyncedTable) genSelectRange() expr.Statement { + return expr.Select(expr.Ident(st.IDColumn)). + From(expr.TableSource(st.TableName)). + Where(expr.MaybeAnd( + st.Filter, + expr.Op(expr.Ident(st.IDColumn), expr.GE, expr.Bind()), + st.rowIDCutoff())). + OrderBy(expr.Asc(expr.Ident(st.IDColumn))). + Limit(expr.Bind()). + Get() +} + +// genRecentCount generates a SELECT statement that returns the number of rows in the table +// added starting with the specified timestamp, taking into account the filter expression +// and the rowid cutoff. +func (st *SyncedTable) genRecentCount() expr.Statement { + return expr.Select(expr.Call("count", expr.Ident(st.IDColumn))). + From(expr.TableSource(st.TableName)). + Where(expr.MaybeAnd(st.Filter, st.rowIDCutoff(), st.timestampCutoff())). + Get() +} + +// genRecentCount generates a SELECT statement that returns the rows in the table added +// starting with the specified timestamp, taking into account the filter expression and +// the rowid cutoff. +func (st *SyncedTable) genSelectRecent() expr.Statement { + return expr.Select(expr.Ident(st.IDColumn)). + From(expr.TableSource(st.TableName)). + Where(expr.MaybeAnd( + st.Filter, + expr.Op(expr.Ident(st.IDColumn), expr.GE, expr.Bind()), + st.rowIDCutoff(), st.timestampCutoff())). + OrderBy(expr.Asc(expr.Ident(st.IDColumn))). + Limit(expr.Bind()). + Get() +} + +// loadMaxRowID returns the max rowid in the table. +func (st *SyncedTable) loadMaxRowID(db sql.Executor) (maxRowID int64, err error) { + nRows, err := db.Exec( + st.genSelectMaxRowID().String(), nil, + func(st *sql.Statement) bool { + maxRowID = st.ColumnInt64(0) + return true + }) + if nRows != 1 { + return 0, fmt.Errorf("expected 1 row, got %d", nRows) + } + return maxRowID, err +} + +// Snaptshot creates a snapshot of the table based on its current max rowid value. +func (st *SyncedTable) Snapshot(db sql.Executor) (*SyncedTableSnapshot, error) { + maxRowID, err := st.loadMaxRowID(db) + if err != nil { + return nil, err + } + return &SyncedTableSnapshot{st, maxRowID}, nil +} + +// SyncedTableSnapshot represents a snapshot of an append-only table. +// The snapshotting is relies on rowids of the table rows never decreasing +// as new rows are added. +// Each snapshot inherits filter expression from the table, so all the rows relevant to +// the snapshot are always filtered using that expression, if it's specified. +type SyncedTableSnapshot struct { + *SyncedTable + maxRowID int64 +} + +// Load loads all the table rows belonging to a snapshot. +func (sts *SyncedTableSnapshot) Load( + db sql.Executor, + dec func(stmt *sql.Statement) bool, +) error { + _, err := db.Exec( + sts.genSelectAll().String(), + func(stmt *sql.Statement) { + if sts.Binder != nil { + sts.Binder(stmt) + } + stmt.BindInt64(stmt.BindParamCount(), sts.maxRowID) + }, + dec) + return err +} + +// LoadCount returns the number of rows in the snapshot. +func (sts *SyncedTableSnapshot) LoadCount( + db sql.Executor, +) (int, error) { + var count int + _, err := db.Exec( + sts.genCount().String(), + func(stmt *sql.Statement) { + if sts.Binder != nil { + sts.Binder(stmt) + } + stmt.BindInt64(stmt.BindParamCount(), sts.maxRowID) + }, + func(stmt *sql.Statement) bool { + count = stmt.ColumnInt(0) + return true + }) + return count, err +} + +// LoadSinceSnapshot loads rows added since the specified previous snapshot. +func (sts *SyncedTableSnapshot) LoadSinceSnapshot( + db sql.Executor, + prev *SyncedTableSnapshot, + dec func(stmt *sql.Statement) bool, +) error { + _, err := db.Exec( + sts.genSelectAllSinceSnapshot().String(), + func(stmt *sql.Statement) { + if sts.Binder != nil { + sts.Binder(stmt) + } + nParams := stmt.BindParamCount() + stmt.BindInt64(nParams-1, prev.maxRowID+1) + stmt.BindInt64(nParams, sts.maxRowID) + }, + dec) + return err +} + +// LoadRange loads ids starting from the specified one. +// limit specifies the maximum number of ids to load. +func (sts *SyncedTableSnapshot) LoadRange( + db sql.Executor, + fromID rangesync.KeyBytes, + limit int, + dec func(stmt *sql.Statement) bool, +) error { + _, err := db.Exec( + sts.genSelectRange().String(), + func(stmt *sql.Statement) { + if sts.Binder != nil { + sts.Binder(stmt) + } + nParams := stmt.BindParamCount() + stmt.BindBytes(nParams-2, fromID) + stmt.BindInt64(nParams-1, sts.maxRowID) + stmt.BindInt64(nParams, int64(limit)) + }, + dec) + return err +} + +var errNoTimestampColumn = errors.New("no timestamp column") + +// LoadRecentCount returns the number of rows added since the specified timestamp. +func (sts *SyncedTableSnapshot) LoadRecentCount( + db sql.Executor, + since int64, +) (int, error) { + if sts.TimestampColumn == "" { + return 0, errNoTimestampColumn + } + var count int + _, err := db.Exec( + sts.genRecentCount().String(), + func(stmt *sql.Statement) { + if sts.Binder != nil { + sts.Binder(stmt) + } + nParams := stmt.BindParamCount() + stmt.BindInt64(nParams-1, sts.maxRowID) + stmt.BindInt64(nParams, since) + }, + func(stmt *sql.Statement) bool { + count = stmt.ColumnInt(0) + return true + }) + return count, err +} + +// LoadRecent loads rows added since the specified timestamp. +func (sts *SyncedTableSnapshot) LoadRecent( + db sql.Executor, + fromID rangesync.KeyBytes, + limit int, + since int64, + dec func(stmt *sql.Statement) bool, +) error { + if sts.TimestampColumn == "" { + return errNoTimestampColumn + } + _, err := db.Exec( + sts.genSelectRecent().String(), + func(stmt *sql.Statement) { + if sts.Binder != nil { + sts.Binder(stmt) + } + nParams := stmt.BindParamCount() + stmt.BindBytes(nParams-3, fromID) + stmt.BindInt64(nParams-2, sts.maxRowID) + stmt.BindInt64(nParams-1, since) + stmt.BindInt64(nParams, int64(limit)) + }, + dec) + return err +} diff --git a/sync2/sqlstore/syncedtable_test.go b/sync2/sqlstore/syncedtable_test.go new file mode 100644 index 0000000000..4e9fa62574 --- /dev/null +++ b/sync2/sqlstore/syncedtable_test.go @@ -0,0 +1,404 @@ +package sqlstore_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/common/util" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/expr" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" + "github.com/spacemeshos/go-spacemesh/sync2/sqlstore" +) + +func TestSyncedTable_GenSQL(t *testing.T) { + for _, tc := range []struct { + name string + st sqlstore.SyncedTable + all string + count string + maxRowID string + idRange string + recent string + recentCount string + }{ + { + name: "no filter", + st: sqlstore.SyncedTable{ + TableName: "atxs", + IDColumn: "id", + TimestampColumn: "received", + }, + all: `SELECT "id" FROM "atxs" WHERE "rowid" <= ?`, + count: `SELECT count("id") FROM "atxs" WHERE "rowid" <= ?`, + maxRowID: `SELECT max("rowid") FROM "atxs"`, + idRange: `SELECT "id" FROM "atxs" WHERE "id" >= ? AND "rowid" <= ? ` + + `ORDER BY "id" LIMIT ?`, + recent: `SELECT "id" FROM "atxs" WHERE "id" >= ? AND "rowid" <= ? ` + + `AND "received" >= ? ORDER BY "id" LIMIT ?`, + recentCount: `SELECT count("id") FROM "atxs" WHERE "rowid" <= ? ` + + `AND "received" >= ?`, + }, + { + name: "filter", + st: sqlstore.SyncedTable{ + TableName: "atxs", + IDColumn: "id", + Filter: expr.MustParse("epoch = ?"), + TimestampColumn: "received", + }, + all: `SELECT "id" FROM "atxs" WHERE "epoch" = ? AND "rowid" <= ?`, + count: `SELECT count("id") FROM "atxs" WHERE "epoch" = ? AND "rowid" <= ?`, + maxRowID: `SELECT max("rowid") FROM "atxs"`, + idRange: `SELECT "id" FROM "atxs" WHERE "epoch" = ? AND "id" >= ? ` + + `AND "rowid" <= ? ORDER BY "id" LIMIT ?`, + recent: `SELECT "id" FROM "atxs" WHERE "epoch" = ? AND "id" >= ? ` + + `AND "rowid" <= ? AND "received" >= ? ORDER BY "id" LIMIT ?`, + recentCount: `SELECT count("id") FROM "atxs" WHERE "epoch" = ? ` + + `AND "rowid" <= ? AND "received" >= ?`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.all, tc.st.GenSelectAll().String(), "all") + require.Equal(t, tc.count, tc.st.GenCount().String(), "count") + require.Equal(t, tc.maxRowID, tc.st.GenSelectMaxRowID().String(), "maxRowID") + require.Equal(t, tc.idRange, tc.st.GenSelectRange().String(), "idRange") + require.Equal(t, tc.recent, tc.st.GenSelectRecent().String(), "recent") + require.Equal(t, tc.recentCount, tc.st.GenRecentCount().String(), "recentCount") + }) + } +} + +func TestSyncedTable_LoadIDs(t *testing.T) { + var db sql.Database + type row struct { + id string + epoch int + ts int + } + rows := []row{ + {"0451cd036aff0367b07590032da827b516b63a4c1b36ea9a253dcf9a7e084980", 1, 100}, + {"0e75d10a8e98a4307dd9d0427dc1d2ebf9e45b602d159ef62c5da95197159844", 1, 110}, + {"18040e78f834b879a9585fba90f6f5e7394dc3bb27f20829baf6bfc9e1bfe44b", 2, 120}, + {"1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", 2, 150}, + {"1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", 2, 180}, + {"2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", 3, 190}, + {"24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", 3, 220}, + } + + insertRows := func(rows []row) { + for _, r := range rows { + _, err := db.Exec("insert into atxs (id, epoch, received) values (?, ?, ?)", + func(stmt *sql.Statement) { + stmt.BindBytes(1, util.FromHex(r.id)) + stmt.BindInt64(2, int64(r.epoch)) + stmt.BindInt64(3, int64(r.ts)) + }, nil) + require.NoError(t, err) + } + } + + initDB := func() { + db = sql.InMemoryTest(t) + _, err := db.Exec(`create table atxs ( + id char(32) not null primary key, + epoch int, + received int)`, nil, nil) + require.NoError(t, err) + insertRows(rows) + } + + mkDecode := func(ids *[]string) func(stmt *sql.Statement) bool { + return func(stmt *sql.Statement) bool { + id := make(rangesync.KeyBytes, stmt.ColumnLen(0)) + stmt.ColumnBytes(0, id) + *ids = append(*ids, id.String()) + return true + } + } + + loadCount := func(sts *sqlstore.SyncedTableSnapshot) int { + count, err := sts.LoadCount(db) + require.NoError(t, err) + return count + } + + loadIDs := func(sts *sqlstore.SyncedTableSnapshot) []string { + var ids []string + require.NoError(t, sts.Load(db, mkDecode(&ids))) + return ids + } + + loadIDsSince := func(stsNew, stsOld *sqlstore.SyncedTableSnapshot) []string { + var ids []string + require.NoError(t, stsNew.LoadSinceSnapshot(db, stsOld, mkDecode(&ids))) + return ids + } + + loadIDRange := func(sts *sqlstore.SyncedTableSnapshot, from rangesync.KeyBytes, limit int) []string { + var ids []string + require.NoError(t, sts.LoadRange(db, from, limit, mkDecode(&ids))) + return ids + } + + loadRecentCount := func( + sts *sqlstore.SyncedTableSnapshot, + ts int64, + ) int { + count, err := sts.LoadRecentCount(db, ts) + require.NoError(t, err) + return count + } + + loadRecent := func( + sts *sqlstore.SyncedTableSnapshot, + from rangesync.KeyBytes, + limit int, + ts int64, + ) []string { + var ids []string + require.NoError(t, sts.LoadRecent(db, from, limit, ts, mkDecode(&ids))) + return ids + } + + t.Run("no filter", func(t *testing.T) { + initDB() + + st := &sqlstore.SyncedTable{ + TableName: "atxs", + IDColumn: "id", + TimestampColumn: "received", + } + + sts1, err := st.Snapshot(db) + require.NoError(t, err) + + require.Equal(t, 7, loadCount(sts1)) + require.ElementsMatch(t, + []string{ + "0451cd036aff0367b07590032da827b516b63a4c1b36ea9a253dcf9a7e084980", + "0e75d10a8e98a4307dd9d0427dc1d2ebf9e45b602d159ef62c5da95197159844", + "18040e78f834b879a9585fba90f6f5e7394dc3bb27f20829baf6bfc9e1bfe44b", + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + }, + loadIDs(sts1)) + + fromID := util.FromHex("1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55") + require.ElementsMatch(t, + []string{ + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + }, + loadIDRange(sts1, fromID, 100)) + + require.ElementsMatch(t, + []string{ + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + }, loadIDRange(sts1, fromID, 2)) + require.ElementsMatch(t, + []string{ + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + }, loadRecent(sts1, fromID, 3, 180)) + require.Equal(t, 3, loadRecentCount(sts1, 180)) + + insertRows([]row{ + {"2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", 2, 300}, + }) + + // the new row is not included in the first snapshot + require.Equal(t, 7, loadCount(sts1)) + require.ElementsMatch(t, + []string{ + "0451cd036aff0367b07590032da827b516b63a4c1b36ea9a253dcf9a7e084980", + "0e75d10a8e98a4307dd9d0427dc1d2ebf9e45b602d159ef62c5da95197159844", + "18040e78f834b879a9585fba90f6f5e7394dc3bb27f20829baf6bfc9e1bfe44b", + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + }, loadIDs(sts1)) + require.ElementsMatch(t, + []string{ + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + }, + loadIDRange(sts1, fromID, 100)) + require.ElementsMatch(t, + []string{ + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + }, + loadRecent(sts1, fromID, 3, 180)) + + sts2, err := st.Snapshot(db) + require.NoError(t, err) + + require.Equal(t, 8, loadCount(sts2)) + require.ElementsMatch(t, + []string{ + "0451cd036aff0367b07590032da827b516b63a4c1b36ea9a253dcf9a7e084980", + "0e75d10a8e98a4307dd9d0427dc1d2ebf9e45b602d159ef62c5da95197159844", + "18040e78f834b879a9585fba90f6f5e7394dc3bb27f20829baf6bfc9e1bfe44b", + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + "2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", + }, + loadIDs(sts2)) + require.ElementsMatch(t, + []string{ + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + "2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", + }, + loadIDRange(sts2, fromID, 100)) + require.ElementsMatch(t, + []string{ + "2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", + }, + loadIDsSince(sts2, sts1)) + require.ElementsMatch(t, + []string{ + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + "2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", + }, + loadRecent(sts2, fromID, 4, 180)) + require.ElementsMatch(t, + []string{ + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + "2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", + }, + loadRecent(sts2, + util.FromHex("2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b"), + 4, 180)) + require.ElementsMatch(t, + []string{ + "2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b", + "24b31a6acc8cd13b119dd5aa81a6c3803250a8a79eb32231f16b09e0971f1b23", + }, + loadRecent(sts2, + util.FromHex("2023eee75bec75da61ad7644bd43f02b9397a72cf489565cb53a4337975a290b"), + 2, 180)) + }) + + t.Run("filter", func(t *testing.T) { + initDB() + st := &sqlstore.SyncedTable{ + TableName: "atxs", + IDColumn: "id", + TimestampColumn: "received", + Filter: expr.MustParse("epoch = ?"), + Binder: func(stmt *sql.Statement) { + stmt.BindInt64(1, 2) + }, + } + + sts1, err := st.Snapshot(db) + require.NoError(t, err) + + require.Equal(t, 3, loadCount(sts1)) + require.ElementsMatch(t, + []string{ + "18040e78f834b879a9585fba90f6f5e7394dc3bb27f20829baf6bfc9e1bfe44b", + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + }, + loadIDs(sts1)) + + fromID := util.FromHex("1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55") + require.ElementsMatch(t, + []string{ + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + }, + loadIDRange(sts1, fromID, 100)) + require.ElementsMatch(t, + []string{ + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + }, loadIDRange(sts1, fromID, 1)) + require.ElementsMatch(t, + []string{ + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + }, + loadRecent(sts1, fromID, 1, 180)) + + insertRows([]row{ + {"2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", 2, 300}, + }) + + // the new row is not included in the first snapshot + require.Equal(t, 3, loadCount(sts1)) + require.ElementsMatch(t, + []string{ + "18040e78f834b879a9585fba90f6f5e7394dc3bb27f20829baf6bfc9e1bfe44b", + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + }, + loadIDs(sts1)) + require.ElementsMatch(t, + []string{ + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + }, + loadIDRange(sts1, fromID, 100)) + require.ElementsMatch(t, + []string{ + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + }, + loadRecent(sts1, fromID, 1, 180)) + + sts2, err := st.Snapshot(db) + require.NoError(t, err) + + require.Equal(t, 4, loadCount(sts2)) + require.ElementsMatch(t, + []string{ + "18040e78f834b879a9585fba90f6f5e7394dc3bb27f20829baf6bfc9e1bfe44b", + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", + }, + loadIDs(sts2)) + require.ElementsMatch(t, + []string{ + "1a9b743abdabe7970041ba2006c0e8bb51a27b1dbfd1a8c70ef5e7703ddeaa55", + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", + }, + loadIDRange(sts2, fromID, 100)) + require.ElementsMatch(t, + []string{ + "2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", + }, + loadIDsSince(sts2, sts1)) + require.ElementsMatch(t, + []string{ + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + "2664e267650ee22dee7d8c987b5cf44ba5596c78df3db5b99fb0ce79cc649d69", + }, + loadRecent(sts2, fromID, 2, 180)) + require.ElementsMatch(t, + []string{ + "1b49b5a17161995cc288523637bd63af5bed99f4f7188effb702da8a7a4beee1", + }, + loadRecent(sts2, fromID, 1, 180)) + }) +} diff --git a/sync2/sqlstore/testdb.go b/sync2/sqlstore/testdb.go new file mode 100644 index 0000000000..5f5372e635 --- /dev/null +++ b/sync2/sqlstore/testdb.go @@ -0,0 +1,46 @@ +package sqlstore + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" +) + +// CreateDB creates a test database. It is only used for testing. +func CreateDB(t *testing.T, keyLen int) sql.Database { + db := sql.InMemoryTest(t) + _, err := db.Exec( + fmt.Sprintf("create table foo(id char(%d) not null primary key)", keyLen), nil, nil) + require.NoError(t, err) + return db +} + +// InsertDBItems inserts items into a test database. It is only used for testing. +func InsertDBItems(t *testing.T, db sql.Database, content []rangesync.KeyBytes) { + err := db.WithTx(context.Background(), func(tx sql.Transaction) error { + for _, id := range content { + _, err := tx.Exec( + "insert into foo(id) values(?)", + func(stmt *sql.Statement) { + stmt.BindBytes(1, id) + }, nil) + if err != nil { + return err + } + } + return nil + }) + require.NoError(t, err) +} + +// PopulateDB creates a test database and inserts items into it. It is only used for testing. +func PopulateDB(t *testing.T, keyLen int, content []rangesync.KeyBytes) sql.Database { + db := CreateDB(t, keyLen) + InsertDBItems(t, db, content) + return db +} From af95c7fcc89442839571a2ae92c679b9d8ccf2ba Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Sun, 27 Oct 2024 02:43:04 +0400 Subject: [PATCH 04/17] sync2: add description of multipeer reconciliation --- dev-docs/multipeer.excalidraw.gz | Bin 0 -> 6868 bytes dev-docs/multipeer.png | Bin 0 -> 392550 bytes dev-docs/sync2-set-reconciliation.md | 176 +++++++++++++++++++++++++-- 3 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 dev-docs/multipeer.excalidraw.gz create mode 100644 dev-docs/multipeer.png diff --git a/dev-docs/multipeer.excalidraw.gz b/dev-docs/multipeer.excalidraw.gz new file mode 100644 index 0000000000000000000000000000000000000000..354e602473ad5bd2c683ae9df0d562942e1b007a GIT binary patch literal 6868 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 O{Q3V2U*R{|&Hw;{dPisg literal 0 HcmV?d00001 diff --git a/dev-docs/multipeer.png b/dev-docs/multipeer.png new file mode 100644 index 0000000000000000000000000000000000000000..6d366dc4e8ee1af81f25ad71d6444eb80241e0f2 GIT binary patch literal 392550 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%0DCF2MperCS5RsTTki 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 ZGNX6Q#ObQom%rejvb>sH_L<9f{tu>L8#(|0 literal 0 HcmV?d00001 diff --git a/dev-docs/sync2-set-reconciliation.md b/dev-docs/sync2-set-reconciliation.md index a67db60196..2d70d433f6 100644 --- a/dev-docs/sync2-set-reconciliation.md +++ b/dev-docs/sync2-set-reconciliation.md @@ -1,7 +1,7 @@ **Table of Contents** -- [Set Reconciliation Protocol (sync2)](#set-reconciliation-protocol-sync2) +- [Pairwise Set Reconciliation Protocol](#pairwise-set-reconciliation-protocol) - [Basic concepts](#basic-concepts) - [Simplified set reconciliation example](#simplified-set-reconciliation-example) - [Attack mitigation](#attack-mitigation) @@ -27,18 +27,15 @@ - [Redundant ItemChunk messages](#redundant-itemchunk-messages) - [Range checksums](#range-checksums) - [Bloom filters for recent sync](#bloom-filters-for-recent-sync) +- [Multi-peer Reconciliation](#multi-peer-reconciliation) -# Set Reconciliation Protocol (sync2) +# Pairwise Set Reconciliation Protocol -The recursive set reconciliation protocol described in this document is based on +The recursive set reconciliation protocol described in this section is based on [Range-Based Set Reconciliation](https://arxiv.org/pdf/2212.13567.pdf) paper by Aljoscha Meyer. -The multi-peer reconciliation approach is loosely based on -[SREP: Out-Of-Band Sync of Transaction Pools for Large-Scale Blockchains](https://people.bu.edu/staro/2023-ICBC-Novak.pdf) -paper by Novak Boškov, Sevval Simsek, Ari Trachtenberg, and David Starobinski. - ## Basic concepts The set reconciliation protocol is intended to synchronize **ordered sets**. @@ -774,3 +771,168 @@ as we don't need the sets to be exactly same after the recent sync; we just want to bring them closer to each other. That being said, a sufficient size of the Bloom filter needs to be chosen to minimize the number of missed elements. + +# Multi-peer Reconciliation + +The multi-peer reconciliation approach is loosely based on +[SREP: Out-Of-Band Sync of Transaction Pools for Large-Scale Blockchains](https://people.bu.edu/staro/2023-ICBC-Novak.pdf) +paper by Novak Boškov, Sevval Simsek, Ari Trachtenberg, and David Starobinski. + +![Multi-peer set reconciliation](multipeer.png) + +Due to the FPTree data structure being used, the copy operation on a +set is `O(1)`. When synchronizing the local set against the remote +peer's set, we need to make sure the set doesn't change while being +synchronized, except for the recent items being added at the beginning +of the sync. Thus, for the purpose of synchronization against each +remote peer, a separate copy of the original set is made. When new +items are being received during sync with a remote peer, these items +are passed to the fetcher which retrieves the actual data blobs from +the peers, after which the received objects are validated and stored +in the state database. The main set is refreshed from time to time to +include the items that were recently added; this doesn't affect the +derived copies currently in use for sync. + +When picking the peers for the purpose of multi-peer sync, each peer +is [probed](#minhash-based-set-difference-estimation) to determine how +many items it has in its set. The peers with substantially lower +number of items than in the local set (configurable threshold) are not +considered for sync, so as not to place additional load on the peer +which are not fully synced yet, and let them decide on their syncing +strategy on their own. Note that the sync is always bi-directional. + +Synchronization against multiple peers can be done in two modes: +1. Split sync involves splitting the whole range into smaller ones, + one smaller range per peer, and limiting the sync to the + corresponding range of IDs when syncing with each peer. +2. Full sync involves syncing the full set against each peer's full + set. + +The sync strategy is selected based on the set similarity between the +local peers and the peers that have been chosen for sync, as well as +on the number of items in the remote peer sets. Roughly it can be described using +the following diagram: + +```mermaid +stateDiagram-v2 + [*] --> Wait + Wait --> ProbePeers : Timeout + ProbePeers --> Wait : No peers /
all probes failed + ProbePeers --> SplitSync : Enough peers for split +
this one is too different +
last sync was not split + ProbePeers --> FullSync : Too few peers for split /
this one is similar
enough to peers + SplitSync --> Wait : Sync failed + SplitSync --> FullSync : Sync succeeded + FullSync --> Wait : Sync terminated +``` + +The split sync approach helps bringing nodes that went substantially +out of sync relatively quickly while also making sure too much load is +not placed on each of the syncing peers. It somewhat resembles +BitTorrent approach where a file is downloaded from multiple peers, +with different pieces being obtained from different peers, even if +this similarity is rather superficial, as the protocol involved is +very different. The split sync is followed by full sync against the +peers, as in some cases, as with ATXs during cycle gaps, the set might +became somewhat "outdated" while the sync was being done. Below is +a diagram describing split sync sequence: + +```mermaid +sequenceDiagram + Note over A: Check how different is A
from its peers + par + A ->> B: Probe + B ->> A: Sample
sim=0.95 count=10001 + and + A ->> C: Probe + C ->> A: Sample
sim=0.94 count=10002 + and + A ->> D: Probe + D ->> A: Sample
sim=0.94 count=10003 + and + A ->> E: Probe + E ->> A: Sample
sim=0.96 count=10001 + and + A ->> F: Probe + F ->> A: Sample
sim=0.89 count=9000 + end + Note over A: Not enough peers close to this one
Enough peers eligible for split sync
Peer F's count is too low
Proceeding with split sync + par + A <<->> B: Sync [0x00..., 0x40...) + and + A <<->> C: Sync [0x40..., 0x80...) + and + A <<->> D: Sync [0x80..., 0xC0...) + and + A <<->> E: Sync [0xC0..., 0x00...) + end + Note over A: Full sync follows split sync
Syncing against peers that are in sync
is very cheap + par + A <<->> B: Sync [0x00..., 0x00...) + and + A <<->> C: Sync [0x00..., 0x00...) + and + A <<->> D: Sync [0x00..., 0x00...) + and + A <<->> E: Sync [0x00..., 0x00...) + end + Note over A: Node A is in sync with the network +``` + +When some of the peers are too slow, their ranges are additionally +assigned to faster peers that managed to complete their ranges +already. Synchronization against slower peers is not interrupted +though until each range is synced at least once: + +```mermaid +sequenceDiagram + par + A <<->> B: Sync [0x00..., 0x40...) + and + A <<->> C: Sync [0x40..., 0x80...) + and + A <<->> D: Sync [0x80..., 0xC0...) + and + A <<->> E: Sync [0xC0..., 0x00...) + and + Note over A: Peer E being too slow + A <<->> E: Sync [0xC0..., 0x00...) + end +``` + +Full sync is used when this node's set is similar enough to its peers' +sets, or when there's not enough peers for split sync. The full sync +against each peer is more reliable than split sync against the same +peers, so after split sync completes, full sync is always done. The +diagram below illustrates the full sync sequence. + +```mermaid +sequenceDiagram + Note over A: Check how different is A
from its peers + par + A ->> B: Probe + B ->> A: Sample
sim=0.999 count=10001 + and + A ->> C: Probe + C ->> A: Sample
sim=0.999 count=10002 + and + A ->> D: Probe + D ->> A: Sample
sim=0.999 count=10003 + and + A ->> E: Probe + E ->> A: Sample
sim=0.999 count=10001 + and + A ->> F: Probe + F ->> A: Sample
sim=0.090 count=9000 + end + Note over A: Enough peers close to this one
Peer F's count is too low
Proceeding with full sync + par + A <<->> B: Sync [0x00..., 0x00...) + and + A <<->> C: Sync [0x00..., 0x00...) + and + A <<->> D: Sync [0x00..., 0x00...) + and + A <<->> E: Sync [0x00..., 0x00...) + end + Note over A: Node A is in sync with the network +``` From b062eaa86d07ec6db04f2298de3b7dcb76164d23 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Sun, 27 Oct 2024 02:45:35 +0400 Subject: [PATCH 05/17] sync2: doc update --- dev-docs/sync2-set-reconciliation.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dev-docs/sync2-set-reconciliation.md b/dev-docs/sync2-set-reconciliation.md index 2d70d433f6..0ffdbe2cfe 100644 --- a/dev-docs/sync2-set-reconciliation.md +++ b/dev-docs/sync2-set-reconciliation.md @@ -28,6 +28,9 @@ - [Range checksums](#range-checksums) - [Bloom filters for recent sync](#bloom-filters-for-recent-sync) - [Multi-peer Reconciliation](#multi-peer-reconciliation) + - [Deciding on the sync strategy](#deciding-on-the-sync-strategy) + - [Split sync](#split-sync) + - [Full sync](#full-sync) @@ -793,6 +796,8 @@ in the state database. The main set is refreshed from time to time to include the items that were recently added; this doesn't affect the derived copies currently in use for sync. +## Deciding on the sync strategy + When picking the peers for the purpose of multi-peer sync, each peer is [probed](#minhash-based-set-difference-estimation) to determine how many items it has in its set. The peers with substantially lower @@ -825,6 +830,8 @@ stateDiagram-v2 FullSync --> Wait : Sync terminated ``` +## Split sync + The split sync approach helps bringing nodes that went substantially out of sync relatively quickly while also making sure too much load is not placed on each of the syncing peers. It somewhat resembles @@ -899,6 +906,8 @@ sequenceDiagram end ``` +## Full sync + Full sync is used when this node's set is similar enough to its peers' sets, or when there's not enough peers for split sync. The full sync against each peer is more reliable than split sync against the same From ef484c5e227eaa881d4ddbbf00223106bda52472 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Wed, 30 Oct 2024 22:44:14 +0400 Subject: [PATCH 06/17] Merge branch 'develop' into sync2/multipeer --- sync2/multipeer/delim.go | 20 +- sync2/multipeer/dumbset.go | 53 +-- sync2/multipeer/export_test.go | 11 +- sync2/multipeer/interface.go | 35 +- sync2/multipeer/mocks_test.go | 628 +++------------------------- sync2/multipeer/multipeer.go | 286 +++++-------- sync2/multipeer/multipeer_test.go | 27 +- sync2/multipeer/setsyncbase.go | 38 +- sync2/multipeer/setsyncbase_test.go | 11 +- sync2/multipeer/split_sync.go | 2 +- sync2/multipeer/split_sync_test.go | 4 +- sync2/p2p.go | 62 +-- sync2/rangesync/dumbset.go | 32 ++ sync2/rangesync/interface.go | 14 + sync2/rangesync/mocks/mocks.go | 541 ++++++++++++++++++++++++ 15 files changed, 846 insertions(+), 918 deletions(-) create mode 100644 sync2/rangesync/mocks/mocks.go diff --git a/sync2/multipeer/delim.go b/sync2/multipeer/delim.go index 73da72c2c2..1d186d400c 100644 --- a/sync2/multipeer/delim.go +++ b/sync2/multipeer/delim.go @@ -6,17 +6,27 @@ import ( "github.com/spacemeshos/go-spacemesh/sync2/rangesync" ) -func getDelimiters(numPeers, keyLen, maxDepth int) (h []rangesync.KeyBytes) { +// getDelimiters generates keys that can be used as range delimiters for splitting the key +// space among the specified number of peers. maxDepth specifies maximum number of high +// non-zero bits to include in resulting keys, which helps avoiding unaligned splits which +// are more expensive for FPTree data structure. +// keyLen specifies key length in bytes. +// The function returns numPeers-1 keys. The ranges are used for split sync, with each +// range being assigned to a separate peer. The first range begins with zero-valued key +// (k0), represented by KeyBytes of length keyLen consisting entirely of zeroes. +// The ranges to scan are: +// [k0,ks[0]); [k0,ks[1]); ... [k0,ks[numPeers-2]); [ks[numPeers-2],0) +func getDelimiters(numPeers, keyLen, maxDepth int) (ks []rangesync.KeyBytes) { if numPeers < 2 { return nil } mask := uint64(0xffffffffffffffff) << (64 - maxDepth) inc := (uint64(0x80) << 56) / uint64(numPeers) - h = make([]rangesync.KeyBytes, numPeers-1) + ks = make([]rangesync.KeyBytes, numPeers-1) for i, v := 0, uint64(0); i < numPeers-1; i++ { - h[i] = make(rangesync.KeyBytes, keyLen) + ks[i] = make(rangesync.KeyBytes, keyLen) v += inc - binary.BigEndian.PutUint64(h[i], (v<<1)&mask) + binary.BigEndian.PutUint64(ks[i], (v<<1)&mask) } - return h + return ks } diff --git a/sync2/multipeer/dumbset.go b/sync2/multipeer/dumbset.go index 31f37c6c0e..31be03062b 100644 --- a/sync2/multipeer/dumbset.go +++ b/sync2/multipeer/dumbset.go @@ -4,55 +4,10 @@ import ( "github.com/spacemeshos/go-spacemesh/sync2/rangesync" ) -// DumbSet is an unoptimized OrderedSet to be used for testing purposes. -// It builds on rangesync.DumbSet. -type DumbSet struct { - *rangesync.DumbSet -} - -var _ OrderedSet = &DumbSet{} +// QQQQQ: rm +type DumbSet = rangesync.DumbSet -// NewDumbHashSet creates an unoptimized OrderedSet to be used for testing purposes. -// If disableReAdd is true, receiving the same item multiple times will fail. +// QQQQQ: rm func NewDumbHashSet() *DumbSet { - return &DumbSet{ - DumbSet: &rangesync.DumbSet{}, - } -} - -// Advance implements OrderedSet. -func (ds *DumbSet) EnsureLoaded() error { - return nil -} - -// Advance implements OrderedSet. -func (ds *DumbSet) Advance() error { - return nil -} - -// Has implements OrderedSet. -func (ds *DumbSet) Has(k rangesync.KeyBytes) (bool, error) { - var first rangesync.KeyBytes - sr := ds.Items() - for cur := range sr.Seq { - if first == nil { - first = cur - } else if first.Compare(cur) == 0 { - return false, sr.Error() - } - if k.Compare(cur) == 0 { - return true, sr.Error() - } - } - return false, sr.Error() -} - -// Copy implements OrderedSet. -func (ds *DumbSet) Copy(syncScope bool) rangesync.OrderedSet { - return &DumbSet{ds.DumbSet.Copy(syncScope).(*rangesync.DumbSet)} -} - -// Release implements OrderedSet. -func (ds *DumbSet) Release() error { - return nil + return &rangesync.DumbSet{} } diff --git a/sync2/multipeer/export_test.go b/sync2/multipeer/export_test.go index 3b6dcbb328..54f86fce9d 100644 --- a/sync2/multipeer/export_test.go +++ b/sync2/multipeer/export_test.go @@ -12,12 +12,11 @@ type ( ) var ( - WithSyncRunner = withSyncRunner - WithClock = withClock - GetDelimiters = getDelimiters - NewSyncQueue = newSyncQueue - NewSplitSync = newSplitSync - NewSyncList = newSyncList + GetDelimiters = getDelimiters + NewSyncQueue = newSyncQueue + NewSplitSync = newSplitSync + NewSyncList = newSyncList + NewMultiPeerReconcilerInternal = newMultiPeerReconciler ) func (mpr *MultiPeerReconciler) FullSync(ctx context.Context, syncPeers []p2p.Peer) error { diff --git a/sync2/multipeer/interface.go b/sync2/multipeer/interface.go index 43e1ce2177..0bbeae717d 100644 --- a/sync2/multipeer/interface.go +++ b/sync2/multipeer/interface.go @@ -10,31 +10,18 @@ import ( //go:generate mockgen -typed -package=multipeer_test -destination=./mocks_test.go -source=./interface.go -// OrdredSet is an interface for a set that can be synced against a remote peer. -// It extends rangesync.OrderedSet with methods which are needed for multi-peer -// reconciliation. -type OrderedSet interface { - rangesync.OrderedSet - // EnsureLoaded ensures that the set is loaded and ready for use. - // It may do nothing in case of in-memory sets, but may trigger loading - // from database in case of database-backed sets. - EnsureLoaded() error - // Advance advances the set by including the items since the set was last loaded - // or advanced. - Advance() error - // Has returns true if the specified key is present in OrderedSet. - Has(rangesync.KeyBytes) (bool, error) - // Release releases the resources associated with the set. - // Calling Release on a set that is already released is a no-op. - Release() error -} +// QQQQQ: rm +type OrderedSet = rangesync.OrderedSet // SyncBase is a synchronization base which holds the original OrderedSet. +// It is used to derive per-peer PeerSyncers with their own copies of the OrderedSet, +// copy operation being O(1) in terms of memory and time complexity. +// It can also probe peers to decide on the synchronization strategy. type SyncBase interface { // Count returns the number of items in the set. Count() (int, error) // Derive creates a Syncer for the specified peer. - Derive(p p2p.Peer) Syncer + Derive(p p2p.Peer) PeerSyncer // Probe probes the specified peer, obtaining its set fingerprint, // the number of items and the similarity value. Probe(ctx context.Context, p p2p.Peer) (rangesync.ProbeResult, error) @@ -42,8 +29,8 @@ type SyncBase interface { Wait() error } -// Syncer is a synchronization interface for a single peer. -type Syncer interface { +// PeerSyncer is a synchronization interface for a single peer. +type PeerSyncer interface { // Peer returns the peer this syncer is for. Peer() p2p.Peer // Sync synchronizes the set with the peer. @@ -63,19 +50,25 @@ type SyncKeyHandler interface { Commit(peer p2p.Peer, base, new OrderedSet) error } +// PairwiseSyncer is used to probe a peer or sync against a single peer. +// It does not contain a copy of the set. type PairwiseSyncer interface { + // Probe probes the peer using the specified range, to check how different the + // peer's set is from the local set. Probe( ctx context.Context, peer p2p.Peer, os rangesync.OrderedSet, x, y rangesync.KeyBytes, ) (rangesync.ProbeResult, error) + // Sync synchronizes the set with the peer using the specified range. Sync( ctx context.Context, peer p2p.Peer, os rangesync.OrderedSet, x, y rangesync.KeyBytes, ) error + // Serve serves an incoming synchronization request. Serve(context context.Context, stream io.ReadWriter, os rangesync.OrderedSet) error } diff --git a/sync2/multipeer/mocks_test.go b/sync2/multipeer/mocks_test.go index 50fc483615..0dfa1a77a3 100644 --- a/sync2/multipeer/mocks_test.go +++ b/sync2/multipeer/mocks_test.go @@ -13,7 +13,6 @@ import ( context "context" io "io" reflect "reflect" - time "time" p2p "github.com/spacemeshos/go-spacemesh/p2p" multipeer "github.com/spacemeshos/go-spacemesh/sync2/multipeer" @@ -21,529 +20,6 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockOrderedSet is a mock of OrderedSet interface. -type MockOrderedSet struct { - ctrl *gomock.Controller - recorder *MockOrderedSetMockRecorder - isgomock struct{} -} - -// MockOrderedSetMockRecorder is the mock recorder for MockOrderedSet. -type MockOrderedSetMockRecorder struct { - mock *MockOrderedSet -} - -// NewMockOrderedSet creates a new mock instance. -func NewMockOrderedSet(ctrl *gomock.Controller) *MockOrderedSet { - mock := &MockOrderedSet{ctrl: ctrl} - mock.recorder = &MockOrderedSetMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockOrderedSet) EXPECT() *MockOrderedSetMockRecorder { - return m.recorder -} - -// Add mocks base method. -func (m *MockOrderedSet) Add(k rangesync.KeyBytes) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Add", k) - ret0, _ := ret[0].(error) - return ret0 -} - -// Add indicates an expected call of Add. -func (mr *MockOrderedSetMockRecorder) Add(k any) *MockOrderedSetAddCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockOrderedSet)(nil).Add), k) - return &MockOrderedSetAddCall{Call: call} -} - -// MockOrderedSetAddCall wrap *gomock.Call -type MockOrderedSetAddCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetAddCall) Return(arg0 error) *MockOrderedSetAddCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetAddCall) Do(f func(rangesync.KeyBytes) error) *MockOrderedSetAddCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetAddCall) DoAndReturn(f func(rangesync.KeyBytes) error) *MockOrderedSetAddCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// Advance mocks base method. -func (m *MockOrderedSet) Advance() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Advance") - ret0, _ := ret[0].(error) - return ret0 -} - -// Advance indicates an expected call of Advance. -func (mr *MockOrderedSetMockRecorder) Advance() *MockOrderedSetAdvanceCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Advance", reflect.TypeOf((*MockOrderedSet)(nil).Advance)) - return &MockOrderedSetAdvanceCall{Call: call} -} - -// MockOrderedSetAdvanceCall wrap *gomock.Call -type MockOrderedSetAdvanceCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetAdvanceCall) Return(arg0 error) *MockOrderedSetAdvanceCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetAdvanceCall) Do(f func() error) *MockOrderedSetAdvanceCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetAdvanceCall) DoAndReturn(f func() error) *MockOrderedSetAdvanceCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// Copy mocks base method. -func (m *MockOrderedSet) Copy(syncScope bool) rangesync.OrderedSet { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Copy", syncScope) - ret0, _ := ret[0].(rangesync.OrderedSet) - return ret0 -} - -// Copy indicates an expected call of Copy. -func (mr *MockOrderedSetMockRecorder) Copy(syncScope any) *MockOrderedSetCopyCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockOrderedSet)(nil).Copy), syncScope) - return &MockOrderedSetCopyCall{Call: call} -} - -// MockOrderedSetCopyCall wrap *gomock.Call -type MockOrderedSetCopyCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetCopyCall) Return(arg0 rangesync.OrderedSet) *MockOrderedSetCopyCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetCopyCall) Do(f func(bool) rangesync.OrderedSet) *MockOrderedSetCopyCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetCopyCall) DoAndReturn(f func(bool) rangesync.OrderedSet) *MockOrderedSetCopyCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// Empty mocks base method. -func (m *MockOrderedSet) Empty() (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Empty") - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Empty indicates an expected call of Empty. -func (mr *MockOrderedSetMockRecorder) Empty() *MockOrderedSetEmptyCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Empty", reflect.TypeOf((*MockOrderedSet)(nil).Empty)) - return &MockOrderedSetEmptyCall{Call: call} -} - -// MockOrderedSetEmptyCall wrap *gomock.Call -type MockOrderedSetEmptyCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetEmptyCall) Return(arg0 bool, arg1 error) *MockOrderedSetEmptyCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetEmptyCall) Do(f func() (bool, error)) *MockOrderedSetEmptyCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetEmptyCall) DoAndReturn(f func() (bool, error)) *MockOrderedSetEmptyCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// EnsureLoaded mocks base method. -func (m *MockOrderedSet) EnsureLoaded() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EnsureLoaded") - ret0, _ := ret[0].(error) - return ret0 -} - -// EnsureLoaded indicates an expected call of EnsureLoaded. -func (mr *MockOrderedSetMockRecorder) EnsureLoaded() *MockOrderedSetEnsureLoadedCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureLoaded", reflect.TypeOf((*MockOrderedSet)(nil).EnsureLoaded)) - return &MockOrderedSetEnsureLoadedCall{Call: call} -} - -// MockOrderedSetEnsureLoadedCall wrap *gomock.Call -type MockOrderedSetEnsureLoadedCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetEnsureLoadedCall) Return(arg0 error) *MockOrderedSetEnsureLoadedCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetEnsureLoadedCall) Do(f func() error) *MockOrderedSetEnsureLoadedCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetEnsureLoadedCall) DoAndReturn(f func() error) *MockOrderedSetEnsureLoadedCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// GetRangeInfo mocks base method. -func (m *MockOrderedSet) GetRangeInfo(x, y rangesync.KeyBytes) (rangesync.RangeInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRangeInfo", x, y) - ret0, _ := ret[0].(rangesync.RangeInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRangeInfo indicates an expected call of GetRangeInfo. -func (mr *MockOrderedSetMockRecorder) GetRangeInfo(x, y any) *MockOrderedSetGetRangeInfoCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRangeInfo", reflect.TypeOf((*MockOrderedSet)(nil).GetRangeInfo), x, y) - return &MockOrderedSetGetRangeInfoCall{Call: call} -} - -// MockOrderedSetGetRangeInfoCall wrap *gomock.Call -type MockOrderedSetGetRangeInfoCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetGetRangeInfoCall) Return(arg0 rangesync.RangeInfo, arg1 error) *MockOrderedSetGetRangeInfoCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetGetRangeInfoCall) Do(f func(rangesync.KeyBytes, rangesync.KeyBytes) (rangesync.RangeInfo, error)) *MockOrderedSetGetRangeInfoCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetGetRangeInfoCall) DoAndReturn(f func(rangesync.KeyBytes, rangesync.KeyBytes) (rangesync.RangeInfo, error)) *MockOrderedSetGetRangeInfoCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// Has mocks base method. -func (m *MockOrderedSet) Has(arg0 rangesync.KeyBytes) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Has", arg0) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Has indicates an expected call of Has. -func (mr *MockOrderedSetMockRecorder) Has(arg0 any) *MockOrderedSetHasCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Has", reflect.TypeOf((*MockOrderedSet)(nil).Has), arg0) - return &MockOrderedSetHasCall{Call: call} -} - -// MockOrderedSetHasCall wrap *gomock.Call -type MockOrderedSetHasCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetHasCall) Return(arg0 bool, arg1 error) *MockOrderedSetHasCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetHasCall) Do(f func(rangesync.KeyBytes) (bool, error)) *MockOrderedSetHasCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetHasCall) DoAndReturn(f func(rangesync.KeyBytes) (bool, error)) *MockOrderedSetHasCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// Items mocks base method. -func (m *MockOrderedSet) Items() rangesync.SeqResult { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Items") - ret0, _ := ret[0].(rangesync.SeqResult) - return ret0 -} - -// Items indicates an expected call of Items. -func (mr *MockOrderedSetMockRecorder) Items() *MockOrderedSetItemsCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Items", reflect.TypeOf((*MockOrderedSet)(nil).Items)) - return &MockOrderedSetItemsCall{Call: call} -} - -// MockOrderedSetItemsCall wrap *gomock.Call -type MockOrderedSetItemsCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetItemsCall) Return(arg0 rangesync.SeqResult) *MockOrderedSetItemsCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetItemsCall) Do(f func() rangesync.SeqResult) *MockOrderedSetItemsCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetItemsCall) DoAndReturn(f func() rangesync.SeqResult) *MockOrderedSetItemsCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// Receive mocks base method. -func (m *MockOrderedSet) Receive(k rangesync.KeyBytes) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Receive", k) - ret0, _ := ret[0].(error) - return ret0 -} - -// Receive indicates an expected call of Receive. -func (mr *MockOrderedSetMockRecorder) Receive(k any) *MockOrderedSetReceiveCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Receive", reflect.TypeOf((*MockOrderedSet)(nil).Receive), k) - return &MockOrderedSetReceiveCall{Call: call} -} - -// MockOrderedSetReceiveCall wrap *gomock.Call -type MockOrderedSetReceiveCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetReceiveCall) Return(arg0 error) *MockOrderedSetReceiveCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetReceiveCall) Do(f func(rangesync.KeyBytes) error) *MockOrderedSetReceiveCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetReceiveCall) DoAndReturn(f func(rangesync.KeyBytes) error) *MockOrderedSetReceiveCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// Received mocks base method. -func (m *MockOrderedSet) Received() rangesync.SeqResult { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Received") - ret0, _ := ret[0].(rangesync.SeqResult) - return ret0 -} - -// Received indicates an expected call of Received. -func (mr *MockOrderedSetMockRecorder) Received() *MockOrderedSetReceivedCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Received", reflect.TypeOf((*MockOrderedSet)(nil).Received)) - return &MockOrderedSetReceivedCall{Call: call} -} - -// MockOrderedSetReceivedCall wrap *gomock.Call -type MockOrderedSetReceivedCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetReceivedCall) Return(arg0 rangesync.SeqResult) *MockOrderedSetReceivedCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetReceivedCall) Do(f func() rangesync.SeqResult) *MockOrderedSetReceivedCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetReceivedCall) DoAndReturn(f func() rangesync.SeqResult) *MockOrderedSetReceivedCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// Recent mocks base method. -func (m *MockOrderedSet) Recent(since time.Time) (rangesync.SeqResult, int) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Recent", since) - ret0, _ := ret[0].(rangesync.SeqResult) - ret1, _ := ret[1].(int) - return ret0, ret1 -} - -// Recent indicates an expected call of Recent. -func (mr *MockOrderedSetMockRecorder) Recent(since any) *MockOrderedSetRecentCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recent", reflect.TypeOf((*MockOrderedSet)(nil).Recent), since) - return &MockOrderedSetRecentCall{Call: call} -} - -// MockOrderedSetRecentCall wrap *gomock.Call -type MockOrderedSetRecentCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetRecentCall) Return(arg0 rangesync.SeqResult, arg1 int) *MockOrderedSetRecentCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetRecentCall) Do(f func(time.Time) (rangesync.SeqResult, int)) *MockOrderedSetRecentCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetRecentCall) DoAndReturn(f func(time.Time) (rangesync.SeqResult, int)) *MockOrderedSetRecentCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// Release mocks base method. -func (m *MockOrderedSet) Release() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Release") - ret0, _ := ret[0].(error) - return ret0 -} - -// Release indicates an expected call of Release. -func (mr *MockOrderedSetMockRecorder) Release() *MockOrderedSetReleaseCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockOrderedSet)(nil).Release)) - return &MockOrderedSetReleaseCall{Call: call} -} - -// MockOrderedSetReleaseCall wrap *gomock.Call -type MockOrderedSetReleaseCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetReleaseCall) Return(arg0 error) *MockOrderedSetReleaseCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetReleaseCall) Do(f func() error) *MockOrderedSetReleaseCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetReleaseCall) DoAndReturn(f func() error) *MockOrderedSetReleaseCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - -// SplitRange mocks base method. -func (m *MockOrderedSet) SplitRange(x, y rangesync.KeyBytes, count int) (rangesync.SplitInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SplitRange", x, y, count) - ret0, _ := ret[0].(rangesync.SplitInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SplitRange indicates an expected call of SplitRange. -func (mr *MockOrderedSetMockRecorder) SplitRange(x, y, count any) *MockOrderedSetSplitRangeCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SplitRange", reflect.TypeOf((*MockOrderedSet)(nil).SplitRange), x, y, count) - return &MockOrderedSetSplitRangeCall{Call: call} -} - -// MockOrderedSetSplitRangeCall wrap *gomock.Call -type MockOrderedSetSplitRangeCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockOrderedSetSplitRangeCall) Return(arg0 rangesync.SplitInfo, arg1 error) *MockOrderedSetSplitRangeCall { - c.Call = c.Call.Return(arg0, arg1) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockOrderedSetSplitRangeCall) Do(f func(rangesync.KeyBytes, rangesync.KeyBytes, int) (rangesync.SplitInfo, error)) *MockOrderedSetSplitRangeCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockOrderedSetSplitRangeCall) DoAndReturn(f func(rangesync.KeyBytes, rangesync.KeyBytes, int) (rangesync.SplitInfo, error)) *MockOrderedSetSplitRangeCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // MockSyncBase is a mock of SyncBase interface. type MockSyncBase struct { ctrl *gomock.Controller @@ -608,10 +84,10 @@ func (c *MockSyncBaseCountCall) DoAndReturn(f func() (int, error)) *MockSyncBase } // Derive mocks base method. -func (m *MockSyncBase) Derive(p p2p.Peer) multipeer.Syncer { +func (m *MockSyncBase) Derive(p p2p.Peer) multipeer.PeerSyncer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Derive", p) - ret0, _ := ret[0].(multipeer.Syncer) + ret0, _ := ret[0].(multipeer.PeerSyncer) return ret0 } @@ -628,19 +104,19 @@ type MockSyncBaseDeriveCall struct { } // Return rewrite *gomock.Call.Return -func (c *MockSyncBaseDeriveCall) Return(arg0 multipeer.Syncer) *MockSyncBaseDeriveCall { +func (c *MockSyncBaseDeriveCall) Return(arg0 multipeer.PeerSyncer) *MockSyncBaseDeriveCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockSyncBaseDeriveCall) Do(f func(p2p.Peer) multipeer.Syncer) *MockSyncBaseDeriveCall { +func (c *MockSyncBaseDeriveCall) Do(f func(p2p.Peer) multipeer.PeerSyncer) *MockSyncBaseDeriveCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockSyncBaseDeriveCall) DoAndReturn(f func(p2p.Peer) multipeer.Syncer) *MockSyncBaseDeriveCall { +func (c *MockSyncBaseDeriveCall) DoAndReturn(f func(p2p.Peer) multipeer.PeerSyncer) *MockSyncBaseDeriveCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -722,32 +198,32 @@ func (c *MockSyncBaseWaitCall) DoAndReturn(f func() error) *MockSyncBaseWaitCall return c } -// MockSyncer is a mock of Syncer interface. -type MockSyncer struct { +// MockPeerSyncer is a mock of PeerSyncer interface. +type MockPeerSyncer struct { ctrl *gomock.Controller - recorder *MockSyncerMockRecorder + recorder *MockPeerSyncerMockRecorder isgomock struct{} } -// MockSyncerMockRecorder is the mock recorder for MockSyncer. -type MockSyncerMockRecorder struct { - mock *MockSyncer +// MockPeerSyncerMockRecorder is the mock recorder for MockPeerSyncer. +type MockPeerSyncerMockRecorder struct { + mock *MockPeerSyncer } -// NewMockSyncer creates a new mock instance. -func NewMockSyncer(ctrl *gomock.Controller) *MockSyncer { - mock := &MockSyncer{ctrl: ctrl} - mock.recorder = &MockSyncerMockRecorder{mock} +// NewMockPeerSyncer creates a new mock instance. +func NewMockPeerSyncer(ctrl *gomock.Controller) *MockPeerSyncer { + mock := &MockPeerSyncer{ctrl: ctrl} + mock.recorder = &MockPeerSyncerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockSyncer) EXPECT() *MockSyncerMockRecorder { +func (m *MockPeerSyncer) EXPECT() *MockPeerSyncerMockRecorder { return m.recorder } // Peer mocks base method. -func (m *MockSyncer) Peer() p2p.Peer { +func (m *MockPeerSyncer) Peer() p2p.Peer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Peer") ret0, _ := ret[0].(p2p.Peer) @@ -755,37 +231,37 @@ func (m *MockSyncer) Peer() p2p.Peer { } // Peer indicates an expected call of Peer. -func (mr *MockSyncerMockRecorder) Peer() *MockSyncerPeerCall { +func (mr *MockPeerSyncerMockRecorder) Peer() *MockPeerSyncerPeerCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peer", reflect.TypeOf((*MockSyncer)(nil).Peer)) - return &MockSyncerPeerCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Peer", reflect.TypeOf((*MockPeerSyncer)(nil).Peer)) + return &MockPeerSyncerPeerCall{Call: call} } -// MockSyncerPeerCall wrap *gomock.Call -type MockSyncerPeerCall struct { +// MockPeerSyncerPeerCall wrap *gomock.Call +type MockPeerSyncerPeerCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockSyncerPeerCall) Return(arg0 p2p.Peer) *MockSyncerPeerCall { +func (c *MockPeerSyncerPeerCall) Return(arg0 p2p.Peer) *MockPeerSyncerPeerCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockSyncerPeerCall) Do(f func() p2p.Peer) *MockSyncerPeerCall { +func (c *MockPeerSyncerPeerCall) Do(f func() p2p.Peer) *MockPeerSyncerPeerCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockSyncerPeerCall) DoAndReturn(f func() p2p.Peer) *MockSyncerPeerCall { +func (c *MockPeerSyncerPeerCall) DoAndReturn(f func() p2p.Peer) *MockPeerSyncerPeerCall { c.Call = c.Call.DoAndReturn(f) return c } // Release mocks base method. -func (m *MockSyncer) Release() error { +func (m *MockPeerSyncer) Release() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Release") ret0, _ := ret[0].(error) @@ -793,37 +269,37 @@ func (m *MockSyncer) Release() error { } // Release indicates an expected call of Release. -func (mr *MockSyncerMockRecorder) Release() *MockSyncerReleaseCall { +func (mr *MockPeerSyncerMockRecorder) Release() *MockPeerSyncerReleaseCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockSyncer)(nil).Release)) - return &MockSyncerReleaseCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockPeerSyncer)(nil).Release)) + return &MockPeerSyncerReleaseCall{Call: call} } -// MockSyncerReleaseCall wrap *gomock.Call -type MockSyncerReleaseCall struct { +// MockPeerSyncerReleaseCall wrap *gomock.Call +type MockPeerSyncerReleaseCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockSyncerReleaseCall) Return(arg0 error) *MockSyncerReleaseCall { +func (c *MockPeerSyncerReleaseCall) Return(arg0 error) *MockPeerSyncerReleaseCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockSyncerReleaseCall) Do(f func() error) *MockSyncerReleaseCall { +func (c *MockPeerSyncerReleaseCall) Do(f func() error) *MockPeerSyncerReleaseCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockSyncerReleaseCall) DoAndReturn(f func() error) *MockSyncerReleaseCall { +func (c *MockPeerSyncerReleaseCall) DoAndReturn(f func() error) *MockPeerSyncerReleaseCall { c.Call = c.Call.DoAndReturn(f) return c } // Serve mocks base method. -func (m *MockSyncer) Serve(ctx context.Context, stream io.ReadWriter) error { +func (m *MockPeerSyncer) Serve(ctx context.Context, stream io.ReadWriter) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Serve", ctx, stream) ret0, _ := ret[0].(error) @@ -831,37 +307,37 @@ func (m *MockSyncer) Serve(ctx context.Context, stream io.ReadWriter) error { } // Serve indicates an expected call of Serve. -func (mr *MockSyncerMockRecorder) Serve(ctx, stream any) *MockSyncerServeCall { +func (mr *MockPeerSyncerMockRecorder) Serve(ctx, stream any) *MockPeerSyncerServeCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Serve", reflect.TypeOf((*MockSyncer)(nil).Serve), ctx, stream) - return &MockSyncerServeCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Serve", reflect.TypeOf((*MockPeerSyncer)(nil).Serve), ctx, stream) + return &MockPeerSyncerServeCall{Call: call} } -// MockSyncerServeCall wrap *gomock.Call -type MockSyncerServeCall struct { +// MockPeerSyncerServeCall wrap *gomock.Call +type MockPeerSyncerServeCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockSyncerServeCall) Return(arg0 error) *MockSyncerServeCall { +func (c *MockPeerSyncerServeCall) Return(arg0 error) *MockPeerSyncerServeCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockSyncerServeCall) Do(f func(context.Context, io.ReadWriter) error) *MockSyncerServeCall { +func (c *MockPeerSyncerServeCall) Do(f func(context.Context, io.ReadWriter) error) *MockPeerSyncerServeCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockSyncerServeCall) DoAndReturn(f func(context.Context, io.ReadWriter) error) *MockSyncerServeCall { +func (c *MockPeerSyncerServeCall) DoAndReturn(f func(context.Context, io.ReadWriter) error) *MockPeerSyncerServeCall { c.Call = c.Call.DoAndReturn(f) return c } // Sync mocks base method. -func (m *MockSyncer) Sync(ctx context.Context, x, y rangesync.KeyBytes) error { +func (m *MockPeerSyncer) Sync(ctx context.Context, x, y rangesync.KeyBytes) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Sync", ctx, x, y) ret0, _ := ret[0].(error) @@ -869,31 +345,31 @@ func (m *MockSyncer) Sync(ctx context.Context, x, y rangesync.KeyBytes) error { } // Sync indicates an expected call of Sync. -func (mr *MockSyncerMockRecorder) Sync(ctx, x, y any) *MockSyncerSyncCall { +func (mr *MockPeerSyncerMockRecorder) Sync(ctx, x, y any) *MockPeerSyncerSyncCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sync", reflect.TypeOf((*MockSyncer)(nil).Sync), ctx, x, y) - return &MockSyncerSyncCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sync", reflect.TypeOf((*MockPeerSyncer)(nil).Sync), ctx, x, y) + return &MockPeerSyncerSyncCall{Call: call} } -// MockSyncerSyncCall wrap *gomock.Call -type MockSyncerSyncCall struct { +// MockPeerSyncerSyncCall wrap *gomock.Call +type MockPeerSyncerSyncCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MockSyncerSyncCall) Return(arg0 error) *MockSyncerSyncCall { +func (c *MockPeerSyncerSyncCall) Return(arg0 error) *MockPeerSyncerSyncCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MockSyncerSyncCall) Do(f func(context.Context, rangesync.KeyBytes, rangesync.KeyBytes) error) *MockSyncerSyncCall { +func (c *MockPeerSyncerSyncCall) Do(f func(context.Context, rangesync.KeyBytes, rangesync.KeyBytes) error) *MockPeerSyncerSyncCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockSyncerSyncCall) DoAndReturn(f func(context.Context, rangesync.KeyBytes, rangesync.KeyBytes) error) *MockSyncerSyncCall { +func (c *MockPeerSyncerSyncCall) DoAndReturn(f func(context.Context, rangesync.KeyBytes, rangesync.KeyBytes) error) *MockPeerSyncerSyncCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/sync2/multipeer/multipeer.go b/sync2/multipeer/multipeer.go index 67ad2c9925..4489445440 100644 --- a/sync2/multipeer/multipeer.go +++ b/sync2/multipeer/multipeer.go @@ -23,126 +23,6 @@ type syncability struct { nearFullCount int } -// MultiPeerReconcilerOpt specifies an option for a MultiPeerReconciler. -type MultiPeerReconcilerOpt func(mpr *MultiPeerReconciler) - -// WithSyncPeerCount sets the number of peers to pick for synchronization. -// Synchronization will still happen if fewer peers are available. -func WithSyncPeerCount(count int) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.syncPeerCount = count - } -} - -// WithMinSplitSyncCount sets the minimum number of items that a peer must have to be -// eligible for split sync (subrange-per-peer). -func WithMinSplitSyncCount(count int) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.minSplitSyncCount = count - } -} - -// WithMaxFullDiff specifies the maximum approximate size of symmetric difference between -// the local set and the remote one for the sets to be considered "mostly in sync", so -// that full sync is preferred to split sync. -func WithMaxFullDiff(diff int) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.maxFullDiff = diff - } -} - -// WithMaxSyncDiff specifies the maximum number of items that a peer can have less than the -// local set for it to be considered for synchronization. -func WithMaxSyncDiff(diff int) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.maxSyncDiff = diff - } -} - -// WithSyncInterval specifies the interval between syncs. -func WithSyncInterval(d time.Duration) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.syncInterval = d - } -} - -// WithRetryInterval specifies the interval between retries after a failed sync. -func WithRetryInterval(d time.Duration) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.retryInterval = d - } -} - -// WithNoPeersRecheckInterval specifies the interval between rechecking for peers after no -// synchronization peers were found. -func WithNoPeersRecheckInterval(d time.Duration) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.noPeersRecheckInterval = d - } -} - -// WithMinSplitSyncPeers specifies the minimum number of peers for the split -// sync to happen. -func WithMinSplitSyncPeers(n int) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.minSplitSyncPeers = n - } -} - -// WithMinCompleteFraction specifies the minimum fraction (0..1) of "mostly synced" peers -// starting with which full sync is used instead of split sync. -func WithMinCompleteFraction(f float64) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.minCompleteFraction = f - } -} - -// WithSplitSyncGracePeriod specifies grace period for split sync peers. -// If a peer doesn't complete syncing its range within the specified duration during split -// sync, its range is assigned additionally to another quicker peer. The sync against the -// "slow" peer is NOT stopped immediately after that. -func WithSplitSyncGracePeriod(t time.Duration) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.splitSyncGracePeriod = t - } -} - -// WithLogger specifies the logger for the MultiPeerReconciler. -func WithLogger(logger *zap.Logger) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.logger = logger - } -} - -// WithMinFullSyncednessCount sets the minimum number of full syncs that must -// have happened within the fullSyncednessPeriod for the node to be considered -// fully synced. -func WithMinFullSyncednessCount(count int) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.minFullSyncednessCount = count - } -} - -// WithFullSyncednessPeriod sets the duration within which the minimum number -// of full syncs must have happened for the node to be considered fully synced. -func WithFullSyncednessPeriod(d time.Duration) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.fullSyncednessPeriod = d - } -} - -func withClock(clock clockwork.Clock) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.clock = clock - } -} - -func withSyncRunner(runner syncRunner) MultiPeerReconcilerOpt { - return func(mpr *MultiPeerReconciler) { - mpr.runner = runner - } -} - type runner struct { mpr *MultiPeerReconciler } @@ -152,7 +32,7 @@ var _ syncRunner = &runner{} func (r *runner) SplitSync(ctx context.Context, syncPeers []p2p.Peer) error { s := newSplitSync( r.mpr.logger, r.mpr.syncBase, r.mpr.peers, syncPeers, - r.mpr.splitSyncGracePeriod, r.mpr.clock, r.mpr.keyLen, r.mpr.maxDepth) + r.mpr.cfg.SplitSyncGracePeriod, r.mpr.clock, r.mpr.keyLen, r.mpr.maxDepth) return s.Sync(ctx) } @@ -160,67 +40,115 @@ func (r *runner) FullSync(ctx context.Context, syncPeers []p2p.Peer) error { return r.mpr.fullSync(ctx, syncPeers) } +// MultiPeerReconcilerConfig contains the configuration for a MultiPeerReconciler. +type MultiPeerReconcilerConfig struct { + // Number of peers to pick for synchronization. + // Synchronization will still happen if fewer peers are available. + SyncPeerCount int `mapstructure:"sync-peer-count"` + // Minimum number of peers for the split sync to happen. + MinSplitSyncPeers int `mapstructure:"min-split-sync-peers"` + // Minimum number of items that a peer must have to be eligible for split sync + // (subrange-per-peer). + MinSplitSyncCount int `mapstructure:"min-split-sync-count"` + // Maximum approximate size of symmetric difference between the local set and the + // remote one for the sets to be considered "mostly in sync", so that full sync is + // preferred to split sync. + MaxFullDiff int `mapstructure:"max-full-diff"` + // Maximum number of items that a peer can have less than the local set for it to + // be considered for synchronization. + MaxSyncDiff int `mapstructure:"max-sync-diff"` + // Minimum fraction (0..1) of "mostly synced" peers starting with which full sync + // is used instead of split sync. + MinCompleteFraction float64 `mapstructure:"min-complete-fraction"` + // Interval between syncs. + SyncInterval time.Duration `mapstructure:"sync-interval"` + // Interval between retries after a failed sync. + RetryInterval time.Duration `mapstructure:"retry-interval"` + // Interval between rechecking for peers after no synchronization peers were + // found. + NoPeersRecheckInterval time.Duration `mapstructure:"no-peers-recheck-interval"` + // Grace period for split sync peers. + // If a peer doesn't complete syncing its range within the specified duration + // during split sync, its range is assigned additionally to another quicker + // peer. The sync against the "slow" peer is NOT stopped immediately after that. + SplitSyncGracePeriod time.Duration `mapstructure:"split-sync-grace-period"` + // Minimum number of full syncs that must have happened within the + // fullSyncednessPeriod for the node to be considered fully synced + MinFullSyncednessCount int `mapstructure:"min-full-syncedness-count"` + // Duration within which the minimum number of full syncs must have happened for + // the node to be considered fully synced. + FullSyncednessPeriod time.Duration `mapstructure:"full-syncedness-count"` +} + +func DefaultConfig() MultiPeerReconcilerConfig { + return MultiPeerReconcilerConfig{ + SyncPeerCount: 20, + MinSplitSyncPeers: 2, + MinSplitSyncCount: 1000, + MaxFullDiff: 10000, + MaxSyncDiff: 100, + SyncInterval: 5 * time.Minute, + RetryInterval: 1 * time.Minute, + NoPeersRecheckInterval: 30 * time.Second, + SplitSyncGracePeriod: time.Minute, + MinCompleteFraction: 0.5, + MinFullSyncednessCount: 1, + FullSyncednessPeriod: 15 * time.Minute, + } +} + // MultiPeerReconciler reconcilies the local set against multiple remote sets. type MultiPeerReconciler struct { - logger *zap.Logger - syncBase SyncBase - peers *peers.Peers - syncPeerCount int - minSplitSyncPeers int - minSplitSyncCount int - maxFullDiff int - maxSyncDiff int - minCompleteFraction float64 - splitSyncGracePeriod time.Duration - syncInterval time.Duration - retryInterval time.Duration - noPeersRecheckInterval time.Duration - clock clockwork.Clock - keyLen int - maxDepth int - runner syncRunner - minFullSyncednessCount int - fullSyncednessPeriod time.Duration - sl *syncList + logger *zap.Logger + cfg MultiPeerReconcilerConfig + syncBase SyncBase + peers *peers.Peers + clock clockwork.Clock + keyLen int + maxDepth int + runner syncRunner + sl *syncList } -// NewMultiPeerReconciler creates a new MultiPeerReconciler. -func NewMultiPeerReconciler( +func newMultiPeerReconciler( + logger *zap.Logger, + cfg MultiPeerReconcilerConfig, syncBase SyncBase, peers *peers.Peers, keyLen, maxDepth int, - opts ...MultiPeerReconcilerOpt, + syncRunner syncRunner, + clock clockwork.Clock, ) *MultiPeerReconciler { mpr := &MultiPeerReconciler{ - logger: zap.NewNop(), - syncBase: syncBase, - peers: peers, - syncPeerCount: 20, - minSplitSyncPeers: 2, - minSplitSyncCount: 1000, - maxFullDiff: 10000, - maxSyncDiff: 100, - syncInterval: 5 * time.Minute, - retryInterval: 1 * time.Minute, - minCompleteFraction: 0.5, - splitSyncGracePeriod: time.Minute, - noPeersRecheckInterval: 30 * time.Second, - clock: clockwork.NewRealClock(), - keyLen: keyLen, - maxDepth: maxDepth, - minFullSyncednessCount: 1, // TODO: use at least 3 and make it configurable - fullSyncednessPeriod: 15 * time.Minute, - } - for _, opt := range opts { - opt(mpr) + logger: zap.NewNop(), + cfg: cfg, + syncBase: syncBase, + peers: peers, + clock: clock, + keyLen: keyLen, + maxDepth: maxDepth, + runner: syncRunner, + sl: newSyncList(clock, cfg.MinFullSyncednessCount, cfg.FullSyncednessPeriod), } if mpr.runner == nil { mpr.runner = &runner{mpr: mpr} } - mpr.sl = newSyncList(mpr.clock, mpr.minFullSyncednessCount, mpr.fullSyncednessPeriod) return mpr } +// NewMultiPeerReconciler creates a new MultiPeerReconciler. +func NewMultiPeerReconciler( + logger *zap.Logger, + cfg MultiPeerReconcilerConfig, + syncBase SyncBase, + peers *peers.Peers, + keyLen, maxDepth int, +) *MultiPeerReconciler { + return newMultiPeerReconciler( + logger, cfg, syncBase, peers, keyLen, maxDepth, + nil, clockwork.NewRealClock()) +} + func (mpr *MultiPeerReconciler) probePeers(ctx context.Context, syncPeers []p2p.Peer) (syncability, error) { var s syncability s.syncable = nil @@ -245,7 +173,7 @@ func (mpr *MultiPeerReconciler) probePeers(ctx context.Context, syncPeers []p2p. // We do not consider peers with substantially fewer items than the local // set for active sync. It's these peers' responsibility to request sync // against this node. - if pr.Count+mpr.maxSyncDiff < c { + if pr.Count+mpr.cfg.MaxSyncDiff < c { mpr.logger.Debug("skipping peer with low item count", zap.Int("peerCount", pr.Count), zap.Int("localCount", c)) @@ -253,7 +181,7 @@ func (mpr *MultiPeerReconciler) probePeers(ctx context.Context, syncPeers []p2p. } s.syncable = append(s.syncable, p) - if pr.Count > mpr.minSplitSyncCount { + if pr.Count > mpr.cfg.MinSplitSyncCount { mpr.logger.Debug("splitSyncable peer", zap.Stringer("peer", p), zap.Int("count", pr.Count)) @@ -264,7 +192,7 @@ func (mpr *MultiPeerReconciler) probePeers(ctx context.Context, syncPeers []p2p. zap.Int("count", pr.Count)) } - mDiff := float64(mpr.maxFullDiff) + mDiff := float64(mpr.cfg.MaxFullDiff) if math.Abs(float64(pr.Count-c)) < mDiff && (1-pr.Sim)*float64(c) < mDiff { mpr.logger.Debug("nearFull peer", zap.Stringer("peer", p), @@ -282,21 +210,21 @@ func (mpr *MultiPeerReconciler) probePeers(ctx context.Context, syncPeers []p2p. } func (mpr *MultiPeerReconciler) needSplitSync(s syncability) bool { - if float64(s.nearFullCount) >= float64(len(s.syncable))*mpr.minCompleteFraction { + if float64(s.nearFullCount) >= float64(len(s.syncable))*mpr.cfg.MinCompleteFraction { // enough peers are close to this one according to minhash score, can do // full sync mpr.logger.Debug("enough peers are close to this one, doing full sync", zap.Int("nearFullCount", s.nearFullCount), zap.Int("peerCount", len(s.syncable)), - zap.Float64("minCompleteFraction", mpr.minCompleteFraction)) + zap.Float64("minCompleteFraction", mpr.cfg.MinCompleteFraction)) return false } - if len(s.splitSyncable) < mpr.minSplitSyncPeers { + if len(s.splitSyncable) < mpr.cfg.MinSplitSyncPeers { // would be nice to do split sync, but not enough peers for that mpr.logger.Debug("not enough peers for split sync", zap.Int("splitSyncableCount", len(s.splitSyncable)), - zap.Int("minSplitSyncPeers", mpr.minSplitSyncPeers)) + zap.Int("minSplitSyncPeers", mpr.cfg.MinSplitSyncPeers)) return false } @@ -330,9 +258,9 @@ func (mpr *MultiPeerReconciler) fullSync(ctx context.Context, syncPeers []p2p.Pe func (mpr *MultiPeerReconciler) syncOnce(ctx context.Context, lastWasSplit bool) (full bool, err error) { var s syncability for { - syncPeers := mpr.peers.SelectBest(mpr.syncPeerCount) + syncPeers := mpr.peers.SelectBest(mpr.cfg.SyncPeerCount) mpr.logger.Debug("selected best peers for sync", - zap.Int("syncPeerCount", mpr.syncPeerCount), + zap.Int("syncPeerCount", mpr.cfg.SyncPeerCount), zap.Int("totalPeers", mpr.peers.Total()), zap.Int("numSelected", len(syncPeers))) if len(syncPeers) != 0 { @@ -347,11 +275,11 @@ func (mpr *MultiPeerReconciler) syncOnce(ctx context.Context, lastWasSplit bool) } } - mpr.logger.Debug("no peers found, waiting", zap.Duration("duration", mpr.noPeersRecheckInterval)) + mpr.logger.Debug("no peers found, waiting", zap.Duration("duration", mpr.cfg.NoPeersRecheckInterval)) select { case <-ctx.Done(): return false, ctx.Err() - case <-mpr.clock.After(mpr.noPeersRecheckInterval): + case <-mpr.clock.After(mpr.cfg.NoPeersRecheckInterval): } } @@ -423,14 +351,14 @@ func (mpr *MultiPeerReconciler) Run(ctx context.Context) error { lastWasSplit := false LOOP: for { - interval := mpr.syncInterval + interval := mpr.cfg.SyncInterval full, err = mpr.syncOnce(ctx, lastWasSplit) if err != nil { if errors.Is(err, context.Canceled) { break } mpr.logger.Error("sync failed", zap.Bool("full", full), zap.Error(err)) - interval = mpr.retryInterval + interval = mpr.cfg.RetryInterval } else if !full { // Split sync needs to be followed by a full sync. // Don't wait to have sync move forward quicker. diff --git a/sync2/multipeer/multipeer_test.go b/sync2/multipeer/multipeer_test.go index 4a178a501a..d9e733d399 100644 --- a/sync2/multipeer/multipeer_test.go +++ b/sync2/multipeer/multipeer_test.go @@ -76,17 +76,18 @@ func newMultiPeerSyncTester(t *testing.T) *multiPeerSyncTester { peers: peers.New(), clock: clockwork.NewFakeClock().(fakeClock), } - mt.reconciler = multipeer.NewMultiPeerReconciler(mt.syncBase, mt.peers, 32, 24, - multipeer.WithLogger(zaptest.NewLogger(t)), - multipeer.WithSyncInterval(time.Minute), - multipeer.WithSyncPeerCount(6), - multipeer.WithMinSplitSyncPeers(2), - multipeer.WithMinSplitSyncCount(90), - multipeer.WithMaxFullDiff(20), - multipeer.WithMinCompleteFraction(0.9), - multipeer.WithNoPeersRecheckInterval(10*time.Second), - multipeer.WithSyncRunner(mt.syncRunner), - multipeer.WithClock(mt.clock)) + cfg := multipeer.DefaultConfig() + cfg.SyncInterval = time.Minute + cfg.SyncPeerCount = 6 + cfg.MinSplitSyncPeers = 2 + cfg.MinSplitSyncCount = 90 + cfg.MaxFullDiff = 20 + cfg.MinCompleteFraction = 0.9 + cfg.NoPeersRecheckInterval = 10 * time.Second + mt.reconciler = multipeer.NewMultiPeerReconcilerInternal( + zaptest.NewLogger(t), cfg, + mt.syncBase, mt.peers, 32, 24, + mt.syncRunner, mt.clock) return mt } @@ -141,11 +142,11 @@ func (mt *multiPeerSyncTester) expectFullSync(pl *peerList, times, numFails int) // delegate to the real fullsync return mt.reconciler.FullSync(ctx, peers) }) - mt.syncBase.EXPECT().Derive(gomock.Any()).DoAndReturn(func(p p2p.Peer) multipeer.Syncer { + mt.syncBase.EXPECT().Derive(gomock.Any()).DoAndReturn(func(p p2p.Peer) multipeer.PeerSyncer { mt.mtx.Lock() defer mt.mtx.Unlock() require.Contains(mt, pl.get(), p) - s := NewMockSyncer(mt.ctrl) + s := NewMockPeerSyncer(mt.ctrl) s.EXPECT().Peer().Return(p).AnyTimes() // TODO: do better job at tracking Release() calls s.EXPECT().Release().AnyTimes() diff --git a/sync2/multipeer/setsyncbase.go b/sync2/multipeer/setsyncbase.go index f36ac2c87f..ffda2447e3 100644 --- a/sync2/multipeer/setsyncbase.go +++ b/sync2/multipeer/setsyncbase.go @@ -59,10 +59,10 @@ func (ssb *SetSyncBase) Count() (int, error) { } // Derive implements SyncBase. -func (ssb *SetSyncBase) Derive(p p2p.Peer) Syncer { +func (ssb *SetSyncBase) Derive(p p2p.Peer) PeerSyncer { ssb.mtx.Lock() defer ssb.mtx.Unlock() - return &setSyncer{ + return &peerSetSyncer{ SetSyncBase: ssb, OrderedSet: ssb.os.Copy(true).(OrderedSet), p: p, @@ -133,7 +133,7 @@ func (ssb *SetSyncBase) advance() error { return ssb.os.Advance() } -type setSyncer struct { +type peerSetSyncer struct { *SetSyncBase OrderedSet p p2p.Peer @@ -141,42 +141,42 @@ type setSyncer struct { } var ( - _ Syncer = &setSyncer{} - _ OrderedSet = &setSyncer{} + _ PeerSyncer = &peerSetSyncer{} + _ OrderedSet = &peerSetSyncer{} ) // Peer implements Syncer. -func (ss *setSyncer) Peer() p2p.Peer { - return ss.p +func (pss *peerSetSyncer) Peer() p2p.Peer { + return pss.p } // Sync implements Syncer. -func (ss *setSyncer) Sync(ctx context.Context, x, y rangesync.KeyBytes) error { - if err := ss.ps.Sync(ctx, ss.p, ss, x, y); err != nil { +func (pss *peerSetSyncer) Sync(ctx context.Context, x, y rangesync.KeyBytes) error { + if err := pss.ps.Sync(ctx, pss.p, pss, x, y); err != nil { return err } - return ss.commit() + return pss.commit() } // Serve implements Syncer. -func (ss *setSyncer) Serve(ctx context.Context, stream io.ReadWriter) error { - if err := ss.ps.Serve(ctx, stream, ss); err != nil { +func (pss *peerSetSyncer) Serve(ctx context.Context, stream io.ReadWriter) error { + if err := pss.ps.Serve(ctx, stream, pss); err != nil { return err } - return ss.commit() + return pss.commit() } // Receive implements OrderedSet. -func (ss *setSyncer) Receive(k rangesync.KeyBytes) error { - if err := ss.receiveKey(k, ss.p); err != nil { +func (pss *peerSetSyncer) Receive(k rangesync.KeyBytes) error { + if err := pss.receiveKey(k, pss.p); err != nil { return err } - return ss.OrderedSet.Receive(k) + return pss.OrderedSet.Receive(k) } -func (ss *setSyncer) commit() error { - if err := ss.handler.Commit(ss.p, ss.SetSyncBase.os, ss.OrderedSet); err != nil { +func (pss *peerSetSyncer) commit() error { + if err := pss.handler.Commit(pss.p, pss.SetSyncBase.os, pss.OrderedSet); err != nil { return err } - return ss.SetSyncBase.advance() + return pss.SetSyncBase.advance() } diff --git a/sync2/multipeer/setsyncbase_test.go b/sync2/multipeer/setsyncbase_test.go index ccde7f8ac9..5a38ce2bfa 100644 --- a/sync2/multipeer/setsyncbase_test.go +++ b/sync2/multipeer/setsyncbase_test.go @@ -13,6 +13,7 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/sync2/multipeer" "github.com/spacemeshos/go-spacemesh/sync2/rangesync" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync/mocks" ) type setSyncBaseTester struct { @@ -20,7 +21,7 @@ type setSyncBaseTester struct { ctrl *gomock.Controller ps *MockPairwiseSyncer handler *MockSyncKeyHandler - os *MockOrderedSet + os *mocks.MockOrderedSet ssb *multipeer.SetSyncBase waitMtx sync.Mutex waitChs map[string]chan error @@ -37,7 +38,7 @@ func newSetSyncBaseTester(t *testing.T, os multipeer.OrderedSet) *setSyncBaseTes doneCh: make(chan rangesync.KeyBytes), } if os == nil { - st.os = NewMockOrderedSet(ctrl) + st.os = mocks.NewMockOrderedSet(ctrl) st.os.EXPECT().Items().DoAndReturn(func() rangesync.SeqResult { return rangesync.EmptySeqResult() }).AnyTimes() @@ -65,8 +66,8 @@ func (st *setSyncBaseTester) getWaitCh(k rangesync.KeyBytes) chan error { return ch } -func (st *setSyncBaseTester) expectCopy(addedKeys ...rangesync.KeyBytes) *MockOrderedSet { - copy := NewMockOrderedSet(st.ctrl) +func (st *setSyncBaseTester) expectCopy(addedKeys ...rangesync.KeyBytes) *mocks.MockOrderedSet { + copy := mocks.NewMockOrderedSet(st.ctrl) st.os.EXPECT().Copy(true).DoAndReturn(func(bool) rangesync.OrderedSet { copy.EXPECT().Items().DoAndReturn(func() rangesync.SeqResult { return rangesync.EmptySeqResult() @@ -83,7 +84,7 @@ func (st *setSyncBaseTester) expectCopy(addedKeys ...rangesync.KeyBytes) *MockOr func (st *setSyncBaseTester) expectSync( p p2p.Peer, - ss multipeer.Syncer, + ss multipeer.PeerSyncer, addedKeys ...rangesync.KeyBytes, ) { st.ps.EXPECT().Sync(gomock.Any(), p, ss, nil, nil). diff --git a/sync2/multipeer/split_sync.go b/sync2/multipeer/split_sync.go index ae9c44c0c8..dfec97945e 100644 --- a/sync2/multipeer/split_sync.go +++ b/sync2/multipeer/split_sync.go @@ -15,7 +15,7 @@ import ( ) type syncResult struct { - s Syncer + s PeerSyncer err error } diff --git a/sync2/multipeer/split_sync_test.go b/sync2/multipeer/split_sync_test.go index e10aedabe8..7a8e584009 100644 --- a/sync2/multipeer/split_sync_test.go +++ b/sync2/multipeer/split_sync_test.go @@ -75,8 +75,8 @@ func newTestSplitSync(t testing.TB) *splitSyncTester { for index, p := range tst.syncPeers { tst.syncBase.EXPECT(). Derive(p). - DoAndReturn(func(peer p2p.Peer) multipeer.Syncer { - s := NewMockSyncer(ctrl) + DoAndReturn(func(peer p2p.Peer) multipeer.PeerSyncer { + s := NewMockPeerSyncer(ctrl) s.EXPECT().Peer().Return(p).AnyTimes() // TODO: do better job at tracking Release() calls s.EXPECT().Release().AnyTimes() diff --git a/sync2/p2p.go b/sync2/p2p.go index 562bbf3b4e..5e7801a133 100644 --- a/sync2/p2p.go +++ b/sync2/p2p.go @@ -22,43 +22,29 @@ type Dispatcher = rangesync.Dispatcher // Config contains the configuration for the P2PHashSync. type Config struct { - MaxSendRange int `mapstructure:"max-send-range"` - SampleSize int `mapstructure:"sample-size"` - SyncPeerCount int `mapstructure:"sync-peer-count"` - MinSplitSyncCount int `mapstructure:"min-split-sync-count"` - MaxFullDiff int `mapstructure:"max-full-diff"` - SyncInterval time.Duration `mapstructure:"sync-interval"` - NoPeersRecheckInterval time.Duration `mapstructure:"no-peers-recheck-interval"` - MinSplitSyncPeers int `mapstructure:"min-split-sync-peers"` - MinCompleteFraction float64 `mapstructure:"min-complete-fraction"` - SplitSyncGracePeriod time.Duration `mapstructure:"split-sync-grace-period"` - RecentTimeSpan time.Duration `mapstructure:"recent-time-span"` - EnableActiveSync bool `mapstructure:"enable-active-sync"` - MaxReconcDiff float64 `mapstructure:"max-reconc-diff"` - AutoCommitCount int `mapstructure:"auto-commit-count"` - AutoCommitIdle time.Duration `mapstructure:"auto-commit-idle"` - TrafficLimit int `mapstructure:"traffic-limit"` - MessageLimit int `mapstructure:"message-limit"` + multipeer.MultiPeerReconcilerConfig `mapstructure:",squash"` + MaxSendRange int `mapstructure:"max-send-range"` + SampleSize int `mapstructure:"sample-size"` + RecentTimeSpan time.Duration `mapstructure:"recent-time-span"` + EnableActiveSync bool `mapstructure:"enable-active-sync"` + MaxReconcDiff float64 `mapstructure:"max-reconc-diff"` + AutoCommitCount int `mapstructure:"auto-commit-count"` + AutoCommitIdle time.Duration `mapstructure:"auto-commit-idle"` + TrafficLimit int `mapstructure:"traffic-limit"` + MessageLimit int `mapstructure:"message-limit"` } // DefaultConfig returns the default configuration for the P2PHashSync. func DefaultConfig() Config { return Config{ - MaxSendRange: rangesync.DefaultMaxSendRange, - SampleSize: rangesync.DefaultSampleSize, - SyncPeerCount: 20, - MinSplitSyncPeers: 2, - MinSplitSyncCount: 1000, - MaxFullDiff: 10000, - SyncInterval: 5 * time.Minute, - MinCompleteFraction: 0.5, - SplitSyncGracePeriod: time.Minute, - NoPeersRecheckInterval: 30 * time.Second, - MaxReconcDiff: 0.01, - AutoCommitCount: 10000, - AutoCommitIdle: time.Second, - TrafficLimit: 200_000_000, - MessageLimit: 20_000_000, + MultiPeerReconcilerConfig: multipeer.DefaultConfig(), + MaxSendRange: rangesync.DefaultMaxSendRange, + SampleSize: rangesync.DefaultSampleSize, + MaxReconcDiff: 0.01, + AutoCommitCount: 10000, + AutoCommitIdle: time.Second, + TrafficLimit: 200_000_000, + MessageLimit: 20_000_000, } } @@ -108,16 +94,8 @@ func NewP2PHashSync( }) s.syncBase = multipeer.NewSetSyncBase(ps, s.os, handler) s.reconciler = multipeer.NewMultiPeerReconciler( - s.syncBase, peers, keyLen, maxDepth, - multipeer.WithLogger(logger), - multipeer.WithSyncPeerCount(cfg.SyncPeerCount), - multipeer.WithMinSplitSyncPeers(cfg.MinSplitSyncPeers), - multipeer.WithMinSplitSyncCount(cfg.MinSplitSyncCount), - multipeer.WithMaxFullDiff(cfg.MaxFullDiff), - multipeer.WithSyncInterval(cfg.SyncInterval), - multipeer.WithMinCompleteFraction(cfg.MinCompleteFraction), - multipeer.WithSplitSyncGracePeriod(time.Minute), - multipeer.WithNoPeersRecheckInterval(cfg.NoPeersRecheckInterval)) + logger, cfg.MultiPeerReconcilerConfig, + s.syncBase, peers, keyLen, maxDepth) d.Register(name, s.serve) return s } diff --git a/sync2/rangesync/dumbset.go b/sync2/rangesync/dumbset.go index d40c646ce4..6a1f6134c2 100644 --- a/sync2/rangesync/dumbset.go +++ b/sync2/rangesync/dumbset.go @@ -317,3 +317,35 @@ func (ds *DumbSet) Copy(syncScope bool) OrderedSet { func (ds *DumbSet) Recent(since time.Time) (SeqResult, int) { return EmptySeqResult(), 0 } + +// Advance implements OrderedSet. +func (ds *DumbSet) EnsureLoaded() error { + return nil +} + +// Advance implements OrderedSet. +func (ds *DumbSet) Advance() error { + return nil +} + +// Has implements OrderedSet. +func (ds *DumbSet) Has(k KeyBytes) (bool, error) { + var first KeyBytes + sr := ds.Items() + for cur := range sr.Seq { + if first == nil { + first = cur + } else if first.Compare(cur) == 0 { + return false, sr.Error() + } + if k.Compare(cur) == 0 { + return true, sr.Error() + } + } + return false, sr.Error() +} + +// Release implements OrderedSet. +func (ds *DumbSet) Release() error { + return nil +} diff --git a/sync2/rangesync/interface.go b/sync2/rangesync/interface.go index 7e6a6c087b..7770ab246d 100644 --- a/sync2/rangesync/interface.go +++ b/sync2/rangesync/interface.go @@ -8,6 +8,8 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p/server" ) +//go:generate mockgen -typed -package=mocks -destination=./mocks/mocks.go -source=./interface.go -exclude_interfaces=Requester,SyncMessage,Conduit + // RangeInfo contains information about a range of items in the OrderedSet as returned by // OrderedSet.GetRangeInfo. type RangeInfo struct { @@ -67,6 +69,18 @@ type OrderedSet interface { // timestamp. Some OrderedSet implementations may not have Recent implemented, in // which case it should return an empty sequence. Recent(since time.Time) (SeqResult, int) + // EnsureLoaded ensures that the set is loaded and ready for use. + // It may do nothing in case of in-memory sets, but may trigger loading + // from database in case of database-backed sets. + EnsureLoaded() error + // Advance advances the set by including the items since the set was last loaded + // or advanced. + Advance() error + // Has returns true if the specified key is present in OrderedSet. + Has(KeyBytes) (bool, error) + // Release releases the resources associated with the set. + // Calling Release on a set that is already released is a no-op. + Release() error } type Requester interface { diff --git a/sync2/rangesync/mocks/mocks.go b/sync2/rangesync/mocks/mocks.go new file mode 100644 index 0000000000..280edddb27 --- /dev/null +++ b/sync2/rangesync/mocks/mocks.go @@ -0,0 +1,541 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./interface.go +// +// Generated by this command: +// +// mockgen -typed -package=mocks -destination=./mocks/mocks.go -source=./interface.go -exclude_interfaces=Requester,SyncMessage,Conduit +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + time "time" + + rangesync "github.com/spacemeshos/go-spacemesh/sync2/rangesync" + gomock "go.uber.org/mock/gomock" +) + +// MockOrderedSet is a mock of OrderedSet interface. +type MockOrderedSet struct { + ctrl *gomock.Controller + recorder *MockOrderedSetMockRecorder + isgomock struct{} +} + +// MockOrderedSetMockRecorder is the mock recorder for MockOrderedSet. +type MockOrderedSetMockRecorder struct { + mock *MockOrderedSet +} + +// NewMockOrderedSet creates a new mock instance. +func NewMockOrderedSet(ctrl *gomock.Controller) *MockOrderedSet { + mock := &MockOrderedSet{ctrl: ctrl} + mock.recorder = &MockOrderedSetMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOrderedSet) EXPECT() *MockOrderedSetMockRecorder { + return m.recorder +} + +// Add mocks base method. +func (m *MockOrderedSet) Add(k rangesync.KeyBytes) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Add", k) + ret0, _ := ret[0].(error) + return ret0 +} + +// Add indicates an expected call of Add. +func (mr *MockOrderedSetMockRecorder) Add(k any) *MockOrderedSetAddCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockOrderedSet)(nil).Add), k) + return &MockOrderedSetAddCall{Call: call} +} + +// MockOrderedSetAddCall wrap *gomock.Call +type MockOrderedSetAddCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetAddCall) Return(arg0 error) *MockOrderedSetAddCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetAddCall) Do(f func(rangesync.KeyBytes) error) *MockOrderedSetAddCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetAddCall) DoAndReturn(f func(rangesync.KeyBytes) error) *MockOrderedSetAddCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Advance mocks base method. +func (m *MockOrderedSet) Advance() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Advance") + ret0, _ := ret[0].(error) + return ret0 +} + +// Advance indicates an expected call of Advance. +func (mr *MockOrderedSetMockRecorder) Advance() *MockOrderedSetAdvanceCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Advance", reflect.TypeOf((*MockOrderedSet)(nil).Advance)) + return &MockOrderedSetAdvanceCall{Call: call} +} + +// MockOrderedSetAdvanceCall wrap *gomock.Call +type MockOrderedSetAdvanceCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetAdvanceCall) Return(arg0 error) *MockOrderedSetAdvanceCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetAdvanceCall) Do(f func() error) *MockOrderedSetAdvanceCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetAdvanceCall) DoAndReturn(f func() error) *MockOrderedSetAdvanceCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Copy mocks base method. +func (m *MockOrderedSet) Copy(syncScope bool) rangesync.OrderedSet { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Copy", syncScope) + ret0, _ := ret[0].(rangesync.OrderedSet) + return ret0 +} + +// Copy indicates an expected call of Copy. +func (mr *MockOrderedSetMockRecorder) Copy(syncScope any) *MockOrderedSetCopyCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockOrderedSet)(nil).Copy), syncScope) + return &MockOrderedSetCopyCall{Call: call} +} + +// MockOrderedSetCopyCall wrap *gomock.Call +type MockOrderedSetCopyCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetCopyCall) Return(arg0 rangesync.OrderedSet) *MockOrderedSetCopyCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetCopyCall) Do(f func(bool) rangesync.OrderedSet) *MockOrderedSetCopyCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetCopyCall) DoAndReturn(f func(bool) rangesync.OrderedSet) *MockOrderedSetCopyCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Empty mocks base method. +func (m *MockOrderedSet) Empty() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Empty") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Empty indicates an expected call of Empty. +func (mr *MockOrderedSetMockRecorder) Empty() *MockOrderedSetEmptyCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Empty", reflect.TypeOf((*MockOrderedSet)(nil).Empty)) + return &MockOrderedSetEmptyCall{Call: call} +} + +// MockOrderedSetEmptyCall wrap *gomock.Call +type MockOrderedSetEmptyCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetEmptyCall) Return(arg0 bool, arg1 error) *MockOrderedSetEmptyCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetEmptyCall) Do(f func() (bool, error)) *MockOrderedSetEmptyCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetEmptyCall) DoAndReturn(f func() (bool, error)) *MockOrderedSetEmptyCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// EnsureLoaded mocks base method. +func (m *MockOrderedSet) EnsureLoaded() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureLoaded") + ret0, _ := ret[0].(error) + return ret0 +} + +// EnsureLoaded indicates an expected call of EnsureLoaded. +func (mr *MockOrderedSetMockRecorder) EnsureLoaded() *MockOrderedSetEnsureLoadedCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureLoaded", reflect.TypeOf((*MockOrderedSet)(nil).EnsureLoaded)) + return &MockOrderedSetEnsureLoadedCall{Call: call} +} + +// MockOrderedSetEnsureLoadedCall wrap *gomock.Call +type MockOrderedSetEnsureLoadedCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetEnsureLoadedCall) Return(arg0 error) *MockOrderedSetEnsureLoadedCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetEnsureLoadedCall) Do(f func() error) *MockOrderedSetEnsureLoadedCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetEnsureLoadedCall) DoAndReturn(f func() error) *MockOrderedSetEnsureLoadedCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetRangeInfo mocks base method. +func (m *MockOrderedSet) GetRangeInfo(x, y rangesync.KeyBytes) (rangesync.RangeInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRangeInfo", x, y) + ret0, _ := ret[0].(rangesync.RangeInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRangeInfo indicates an expected call of GetRangeInfo. +func (mr *MockOrderedSetMockRecorder) GetRangeInfo(x, y any) *MockOrderedSetGetRangeInfoCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRangeInfo", reflect.TypeOf((*MockOrderedSet)(nil).GetRangeInfo), x, y) + return &MockOrderedSetGetRangeInfoCall{Call: call} +} + +// MockOrderedSetGetRangeInfoCall wrap *gomock.Call +type MockOrderedSetGetRangeInfoCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetGetRangeInfoCall) Return(arg0 rangesync.RangeInfo, arg1 error) *MockOrderedSetGetRangeInfoCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetGetRangeInfoCall) Do(f func(rangesync.KeyBytes, rangesync.KeyBytes) (rangesync.RangeInfo, error)) *MockOrderedSetGetRangeInfoCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetGetRangeInfoCall) DoAndReturn(f func(rangesync.KeyBytes, rangesync.KeyBytes) (rangesync.RangeInfo, error)) *MockOrderedSetGetRangeInfoCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Has mocks base method. +func (m *MockOrderedSet) Has(arg0 rangesync.KeyBytes) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Has", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Has indicates an expected call of Has. +func (mr *MockOrderedSetMockRecorder) Has(arg0 any) *MockOrderedSetHasCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Has", reflect.TypeOf((*MockOrderedSet)(nil).Has), arg0) + return &MockOrderedSetHasCall{Call: call} +} + +// MockOrderedSetHasCall wrap *gomock.Call +type MockOrderedSetHasCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetHasCall) Return(arg0 bool, arg1 error) *MockOrderedSetHasCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetHasCall) Do(f func(rangesync.KeyBytes) (bool, error)) *MockOrderedSetHasCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetHasCall) DoAndReturn(f func(rangesync.KeyBytes) (bool, error)) *MockOrderedSetHasCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Items mocks base method. +func (m *MockOrderedSet) Items() rangesync.SeqResult { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Items") + ret0, _ := ret[0].(rangesync.SeqResult) + return ret0 +} + +// Items indicates an expected call of Items. +func (mr *MockOrderedSetMockRecorder) Items() *MockOrderedSetItemsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Items", reflect.TypeOf((*MockOrderedSet)(nil).Items)) + return &MockOrderedSetItemsCall{Call: call} +} + +// MockOrderedSetItemsCall wrap *gomock.Call +type MockOrderedSetItemsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetItemsCall) Return(arg0 rangesync.SeqResult) *MockOrderedSetItemsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetItemsCall) Do(f func() rangesync.SeqResult) *MockOrderedSetItemsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetItemsCall) DoAndReturn(f func() rangesync.SeqResult) *MockOrderedSetItemsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Receive mocks base method. +func (m *MockOrderedSet) Receive(k rangesync.KeyBytes) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Receive", k) + ret0, _ := ret[0].(error) + return ret0 +} + +// Receive indicates an expected call of Receive. +func (mr *MockOrderedSetMockRecorder) Receive(k any) *MockOrderedSetReceiveCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Receive", reflect.TypeOf((*MockOrderedSet)(nil).Receive), k) + return &MockOrderedSetReceiveCall{Call: call} +} + +// MockOrderedSetReceiveCall wrap *gomock.Call +type MockOrderedSetReceiveCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetReceiveCall) Return(arg0 error) *MockOrderedSetReceiveCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetReceiveCall) Do(f func(rangesync.KeyBytes) error) *MockOrderedSetReceiveCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetReceiveCall) DoAndReturn(f func(rangesync.KeyBytes) error) *MockOrderedSetReceiveCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Received mocks base method. +func (m *MockOrderedSet) Received() rangesync.SeqResult { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Received") + ret0, _ := ret[0].(rangesync.SeqResult) + return ret0 +} + +// Received indicates an expected call of Received. +func (mr *MockOrderedSetMockRecorder) Received() *MockOrderedSetReceivedCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Received", reflect.TypeOf((*MockOrderedSet)(nil).Received)) + return &MockOrderedSetReceivedCall{Call: call} +} + +// MockOrderedSetReceivedCall wrap *gomock.Call +type MockOrderedSetReceivedCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetReceivedCall) Return(arg0 rangesync.SeqResult) *MockOrderedSetReceivedCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetReceivedCall) Do(f func() rangesync.SeqResult) *MockOrderedSetReceivedCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetReceivedCall) DoAndReturn(f func() rangesync.SeqResult) *MockOrderedSetReceivedCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Recent mocks base method. +func (m *MockOrderedSet) Recent(since time.Time) (rangesync.SeqResult, int) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Recent", since) + ret0, _ := ret[0].(rangesync.SeqResult) + ret1, _ := ret[1].(int) + return ret0, ret1 +} + +// Recent indicates an expected call of Recent. +func (mr *MockOrderedSetMockRecorder) Recent(since any) *MockOrderedSetRecentCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recent", reflect.TypeOf((*MockOrderedSet)(nil).Recent), since) + return &MockOrderedSetRecentCall{Call: call} +} + +// MockOrderedSetRecentCall wrap *gomock.Call +type MockOrderedSetRecentCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetRecentCall) Return(arg0 rangesync.SeqResult, arg1 int) *MockOrderedSetRecentCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetRecentCall) Do(f func(time.Time) (rangesync.SeqResult, int)) *MockOrderedSetRecentCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetRecentCall) DoAndReturn(f func(time.Time) (rangesync.SeqResult, int)) *MockOrderedSetRecentCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Release mocks base method. +func (m *MockOrderedSet) Release() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Release") + ret0, _ := ret[0].(error) + return ret0 +} + +// Release indicates an expected call of Release. +func (mr *MockOrderedSetMockRecorder) Release() *MockOrderedSetReleaseCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*MockOrderedSet)(nil).Release)) + return &MockOrderedSetReleaseCall{Call: call} +} + +// MockOrderedSetReleaseCall wrap *gomock.Call +type MockOrderedSetReleaseCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetReleaseCall) Return(arg0 error) *MockOrderedSetReleaseCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetReleaseCall) Do(f func() error) *MockOrderedSetReleaseCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetReleaseCall) DoAndReturn(f func() error) *MockOrderedSetReleaseCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SplitRange mocks base method. +func (m *MockOrderedSet) SplitRange(x, y rangesync.KeyBytes, count int) (rangesync.SplitInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SplitRange", x, y, count) + ret0, _ := ret[0].(rangesync.SplitInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SplitRange indicates an expected call of SplitRange. +func (mr *MockOrderedSetMockRecorder) SplitRange(x, y, count any) *MockOrderedSetSplitRangeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SplitRange", reflect.TypeOf((*MockOrderedSet)(nil).SplitRange), x, y, count) + return &MockOrderedSetSplitRangeCall{Call: call} +} + +// MockOrderedSetSplitRangeCall wrap *gomock.Call +type MockOrderedSetSplitRangeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrderedSetSplitRangeCall) Return(arg0 rangesync.SplitInfo, arg1 error) *MockOrderedSetSplitRangeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrderedSetSplitRangeCall) Do(f func(rangesync.KeyBytes, rangesync.KeyBytes, int) (rangesync.SplitInfo, error)) *MockOrderedSetSplitRangeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrderedSetSplitRangeCall) DoAndReturn(f func(rangesync.KeyBytes, rangesync.KeyBytes, int) (rangesync.SplitInfo, error)) *MockOrderedSetSplitRangeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} From 0cf16786def4531df8d601bce9106d9cbfcf86b2 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Thu, 31 Oct 2024 03:07:48 +0400 Subject: [PATCH 07/17] sync2: address comments --- sync2/multipeer/delim.go | 2 +- sync2/multipeer/dumbset.go | 13 -- sync2/multipeer/interface.go | 5 +- sync2/multipeer/mocks_test.go | 6 +- sync2/multipeer/multipeer.go | 128 +++++++++------- sync2/multipeer/multipeer_test.go | 25 ++- sync2/multipeer/setsyncbase.go | 43 ++++-- sync2/multipeer/setsyncbase_test.go | 25 ++- sync2/multipeer/split_sync.go | 41 ++--- sync2/p2p.go | 39 ++--- sync2/p2p_test.go | 13 +- sync2/rangesync/dispatcher_test.go | 4 +- sync2/rangesync/export_test.go | 13 +- sync2/rangesync/p2p.go | 65 +++++--- sync2/rangesync/p2p_test.go | 104 +++++++------ sync2/rangesync/rangesync.go | 219 ++++++++++++--------------- sync2/rangesync/rangesync_test.go | 45 +++--- sync2/rangesync/wire_conduit.go | 48 ++---- sync2/rangesync/wire_conduit_test.go | 43 +++--- 19 files changed, 431 insertions(+), 450 deletions(-) delete mode 100644 sync2/multipeer/dumbset.go diff --git a/sync2/multipeer/delim.go b/sync2/multipeer/delim.go index 1d186d400c..6bae9ebc93 100644 --- a/sync2/multipeer/delim.go +++ b/sync2/multipeer/delim.go @@ -15,7 +15,7 @@ import ( // range being assigned to a separate peer. The first range begins with zero-valued key // (k0), represented by KeyBytes of length keyLen consisting entirely of zeroes. // The ranges to scan are: -// [k0,ks[0]); [k0,ks[1]); ... [k0,ks[numPeers-2]); [ks[numPeers-2],0) +// [k0,ks[0]); [k0,ks[1]); ... [k0,ks[numPeers-2]); [ks[numPeers-2],0). func getDelimiters(numPeers, keyLen, maxDepth int) (ks []rangesync.KeyBytes) { if numPeers < 2 { return nil diff --git a/sync2/multipeer/dumbset.go b/sync2/multipeer/dumbset.go deleted file mode 100644 index 31be03062b..0000000000 --- a/sync2/multipeer/dumbset.go +++ /dev/null @@ -1,13 +0,0 @@ -package multipeer - -import ( - "github.com/spacemeshos/go-spacemesh/sync2/rangesync" -) - -// QQQQQ: rm -type DumbSet = rangesync.DumbSet - -// QQQQQ: rm -func NewDumbHashSet() *DumbSet { - return &rangesync.DumbSet{} -} diff --git a/sync2/multipeer/interface.go b/sync2/multipeer/interface.go index 0bbeae717d..ef8dc6f444 100644 --- a/sync2/multipeer/interface.go +++ b/sync2/multipeer/interface.go @@ -10,9 +10,6 @@ import ( //go:generate mockgen -typed -package=multipeer_test -destination=./mocks_test.go -source=./interface.go -// QQQQQ: rm -type OrderedSet = rangesync.OrderedSet - // SyncBase is a synchronization base which holds the original OrderedSet. // It is used to derive per-peer PeerSyncers with their own copies of the OrderedSet, // copy operation being O(1) in terms of memory and time complexity. @@ -47,7 +44,7 @@ type SyncKeyHandler interface { // Receive handles a key that was received from a peer. Receive(k rangesync.KeyBytes, peer p2p.Peer) (bool, error) // Commit is invoked at the end of synchronization to apply the changes. - Commit(peer p2p.Peer, base, new OrderedSet) error + Commit(peer p2p.Peer, base, new rangesync.OrderedSet) error } // PairwiseSyncer is used to probe a peer or sync against a single peer. diff --git a/sync2/multipeer/mocks_test.go b/sync2/multipeer/mocks_test.go index 0dfa1a77a3..ffe4c90d6b 100644 --- a/sync2/multipeer/mocks_test.go +++ b/sync2/multipeer/mocks_test.go @@ -399,7 +399,7 @@ func (m *MockSyncKeyHandler) EXPECT() *MockSyncKeyHandlerMockRecorder { } // Commit mocks base method. -func (m *MockSyncKeyHandler) Commit(peer p2p.Peer, base, new multipeer.OrderedSet) error { +func (m *MockSyncKeyHandler) Commit(peer p2p.Peer, base, new rangesync.OrderedSet) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Commit", peer, base, new) ret0, _ := ret[0].(error) @@ -425,13 +425,13 @@ func (c *MockSyncKeyHandlerCommitCall) Return(arg0 error) *MockSyncKeyHandlerCom } // Do rewrite *gomock.Call.Do -func (c *MockSyncKeyHandlerCommitCall) Do(f func(p2p.Peer, multipeer.OrderedSet, multipeer.OrderedSet) error) *MockSyncKeyHandlerCommitCall { +func (c *MockSyncKeyHandlerCommitCall) Do(f func(p2p.Peer, rangesync.OrderedSet, rangesync.OrderedSet) error) *MockSyncKeyHandlerCommitCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockSyncKeyHandlerCommitCall) DoAndReturn(f func(p2p.Peer, multipeer.OrderedSet, multipeer.OrderedSet) error) *MockSyncKeyHandlerCommitCall { +func (c *MockSyncKeyHandlerCommitCall) DoAndReturn(f func(p2p.Peer, rangesync.OrderedSet, rangesync.OrderedSet) error) *MockSyncKeyHandlerCommitCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/sync2/multipeer/multipeer.go b/sync2/multipeer/multipeer.go index 4489445440..bbc2119775 100644 --- a/sync2/multipeer/multipeer.go +++ b/sync2/multipeer/multipeer.go @@ -12,6 +12,7 @@ import ( "github.com/spacemeshos/go-spacemesh/fetch/peers" "github.com/spacemeshos/go-spacemesh/p2p" + "github.com/spacemeshos/go-spacemesh/sync2/rangesync" ) type syncability struct { @@ -80,6 +81,7 @@ type MultiPeerReconcilerConfig struct { FullSyncednessPeriod time.Duration `mapstructure:"full-syncedness-count"` } +// DefaultConfig returns the default configuration for the MultiPeerReconciler. func DefaultConfig() MultiPeerReconcilerConfig { return MultiPeerReconcilerConfig{ SyncPeerCount: 20, @@ -154,59 +156,82 @@ func (mpr *MultiPeerReconciler) probePeers(ctx context.Context, syncPeers []p2p. s.syncable = nil s.splitSyncable = nil s.nearFullCount = 0 + type probeResult struct { + p p2p.Peer + rangesync.ProbeResult + } + probeCh := make(chan probeResult) + + var eg errgroup.Group for _, p := range syncPeers { - mpr.logger.Debug("probe peer", zap.Stringer("peer", p)) - pr, err := mpr.syncBase.Probe(ctx, p) - if err != nil { - mpr.logger.Warn("error probing the peer", zap.Any("peer", p), zap.Error(err)) - if errors.Is(err, context.Canceled) { - return s, err + eg.Go(func() error { + mpr.logger.Debug("probe peer", zap.Stringer("peer", p)) + pr, err := mpr.syncBase.Probe(ctx, p) + if err != nil { + mpr.logger.Warn("error probing the peer", zap.Any("peer", p), zap.Error(err)) + if errors.Is(err, context.Canceled) { + return err + } + } else { + probeCh <- probeResult{p, pr} } - continue - } + return nil + }) + } - c, err := mpr.syncBase.Count() - if err != nil { - return s, err - } + var egConsume errgroup.Group + egConsume.Go(func() error { + for pr := range probeCh { + c, err := mpr.syncBase.Count() + if err != nil { + return err + } - // We do not consider peers with substantially fewer items than the local - // set for active sync. It's these peers' responsibility to request sync - // against this node. - if pr.Count+mpr.cfg.MaxSyncDiff < c { - mpr.logger.Debug("skipping peer with low item count", - zap.Int("peerCount", pr.Count), - zap.Int("localCount", c)) - continue - } + // We do not consider peers with substantially fewer items than the local + // set for active sync. It's these peers' responsibility to request sync + // against this node. + if pr.Count+mpr.cfg.MaxSyncDiff < c { + mpr.logger.Debug("skipping peer with low item count", + zap.Int("peerCount", pr.Count), + zap.Int("localCount", c)) + continue + } - s.syncable = append(s.syncable, p) - if pr.Count > mpr.cfg.MinSplitSyncCount { - mpr.logger.Debug("splitSyncable peer", - zap.Stringer("peer", p), - zap.Int("count", pr.Count)) - s.splitSyncable = append(s.splitSyncable, p) - } else { - mpr.logger.Debug("NOT splitSyncable peer", - zap.Stringer("peer", p), - zap.Int("count", pr.Count)) - } + s.syncable = append(s.syncable, pr.p) + if pr.Count > mpr.cfg.MinSplitSyncCount { + mpr.logger.Debug("splitSyncable peer", + zap.Stringer("peer", pr.p), + zap.Int("count", pr.Count)) + s.splitSyncable = append(s.splitSyncable, pr.p) + } else { + mpr.logger.Debug("NOT splitSyncable peer", + zap.Stringer("peer", pr.p), + zap.Int("count", pr.Count)) + } - mDiff := float64(mpr.cfg.MaxFullDiff) - if math.Abs(float64(pr.Count-c)) < mDiff && (1-pr.Sim)*float64(c) < mDiff { - mpr.logger.Debug("nearFull peer", - zap.Stringer("peer", p), - zap.Float64("sim", pr.Sim), - zap.Int("localCount", c)) - s.nearFullCount++ - } else { - mpr.logger.Debug("nearFull peer", - zap.Stringer("peer", p), - zap.Float64("sim", pr.Sim), - zap.Int("localCount", c)) + mDiff := float64(mpr.cfg.MaxFullDiff) + if math.Abs(float64(pr.Count-c)) < mDiff && (1-pr.Sim)*float64(c) < mDiff { + mpr.logger.Debug("nearFull peer", + zap.Stringer("peer", pr.p), + zap.Float64("sim", pr.Sim), + zap.Int("localCount", c)) + s.nearFullCount++ + } else { + mpr.logger.Debug("nearFull peer", + zap.Stringer("peer", pr.p), + zap.Float64("sim", pr.Sim), + zap.Int("localCount", c)) + } } + return nil + }) + err := eg.Wait() + close(probeCh) + if err != nil { + egConsume.Wait() + return s, err } - return s, nil + return s, egConsume.Wait() } func (mpr *MultiPeerReconciler) needSplitSync(s syncability) bool { @@ -237,12 +262,12 @@ func (mpr *MultiPeerReconciler) fullSync(ctx context.Context, syncPeers []p2p.Pe for _, p := range syncPeers { syncer := mpr.syncBase.Derive(p) eg.Go(func() error { - defer syncer.Release() err := syncer.Sync(ctx, nil, nil) switch { case err == nil: mpr.sl.NoteSync() case errors.Is(err, context.Canceled): + syncer.Release() return err default: // failing to sync against a particular peer is not considered @@ -343,7 +368,6 @@ func (mpr *MultiPeerReconciler) Run(ctx context.Context) error { // Use peers with > minSplitSyncCount // Wait for all the syncs to complete/fail // All syncs completed (success / fail) => A - ctx, cancel := context.WithCancel(ctx) var ( err error full bool @@ -376,12 +400,10 @@ LOOP: case <-mpr.clock.After(interval): } } - cancel() - if err != nil { - mpr.syncBase.Wait() - return err - } - return mpr.syncBase.Wait() + // The loop is only exited upon context cancellation. + // Thus, syncBase.Wait() is guaranteed not to block indefinitely here. + mpr.syncBase.Wait() + return err } // Synced returns true if the node is considered synced, that is, the specified diff --git a/sync2/multipeer/multipeer_test.go b/sync2/multipeer/multipeer_test.go index d9e733d399..25a1cd3d54 100644 --- a/sync2/multipeer/multipeer_test.go +++ b/sync2/multipeer/multipeer_test.go @@ -66,7 +66,7 @@ type multiPeerSyncTester struct { mtx sync.Mutex } -func newMultiPeerSyncTester(t *testing.T) *multiPeerSyncTester { +func newMultiPeerSyncTester(t *testing.T, addPeers int) *multiPeerSyncTester { ctrl := gomock.NewController(t) mt := &multiPeerSyncTester{ T: t, @@ -88,6 +88,7 @@ func newMultiPeerSyncTester(t *testing.T) *multiPeerSyncTester { zaptest.NewLogger(t), cfg, mt.syncBase, mt.peers, 32, 24, mt.syncRunner, mt.clock) + mt.addPeers(addPeers) return mt } @@ -172,7 +173,7 @@ func TestMultiPeerSync(t *testing.T) { const numSyncs = 3 t.Run("split sync", func(t *testing.T) { - mt := newMultiPeerSyncTester(t) + mt := newMultiPeerSyncTester(t, 0) ctx := mt.start() mt.clock.BlockUntilContext(ctx, 1) // Advance by sync interval. No peers yet @@ -214,8 +215,7 @@ func TestMultiPeerSync(t *testing.T) { }) t.Run("full sync", func(t *testing.T) { - mt := newMultiPeerSyncTester(t) - mt.addPeers(10) + mt := newMultiPeerSyncTester(t, 10) mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() require.False(t, mt.reconciler.Synced()) var ctx context.Context @@ -242,7 +242,7 @@ func TestMultiPeerSync(t *testing.T) { }) t.Run("full sync, peers with low count ignored", func(t *testing.T) { - mt := newMultiPeerSyncTester(t) + mt := newMultiPeerSyncTester(t, 0) addedPeers := mt.addPeers(6) mt.syncBase.EXPECT().Count().Return(1000, nil).AnyTimes() require.False(t, mt.reconciler.Synced()) @@ -279,8 +279,7 @@ func TestMultiPeerSync(t *testing.T) { }) t.Run("full sync due to low peer count", func(t *testing.T) { - mt := newMultiPeerSyncTester(t) - mt.addPeers(1) + mt := newMultiPeerSyncTester(t, 1) mt.syncBase.EXPECT().Count().Return(50, nil).AnyTimes() var ctx context.Context for i := 0; i < numSyncs; i++ { @@ -304,8 +303,7 @@ func TestMultiPeerSync(t *testing.T) { }) t.Run("probe failure", func(t *testing.T) { - mt := newMultiPeerSyncTester(t) - mt.addPeers(10) + mt := newMultiPeerSyncTester(t, 10) mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() mt.syncBase.EXPECT().Probe(gomock.Any(), gomock.Any()). Return(rangesync.ProbeResult{}, errors.New("probe failed")) @@ -318,8 +316,7 @@ func TestMultiPeerSync(t *testing.T) { }) t.Run("failed peers during full sync", func(t *testing.T) { - mt := newMultiPeerSyncTester(t) - mt.addPeers(10) + mt := newMultiPeerSyncTester(t, 10) mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() var ctx context.Context for i := 0; i < numSyncs; i++ { @@ -339,8 +336,7 @@ func TestMultiPeerSync(t *testing.T) { }) t.Run("failed synced key handling during full sync", func(t *testing.T) { - mt := newMultiPeerSyncTester(t) - mt.addPeers(10) + mt := newMultiPeerSyncTester(t, 10) mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() var ctx context.Context for i := 0; i < numSyncs; i++ { @@ -360,8 +356,7 @@ func TestMultiPeerSync(t *testing.T) { }) t.Run("cancellation during sync", func(t *testing.T) { - mt := newMultiPeerSyncTester(t) - mt.addPeers(10) + mt := newMultiPeerSyncTester(t, 10) mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() mt.expectProbe(6, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) mt.syncRunner.EXPECT().FullSync(gomock.Any(), gomock.Any()).DoAndReturn( diff --git a/sync2/multipeer/setsyncbase.go b/sync2/multipeer/setsyncbase.go index ffda2447e3..3aadd6344a 100644 --- a/sync2/multipeer/setsyncbase.go +++ b/sync2/multipeer/setsyncbase.go @@ -7,6 +7,7 @@ import ( "io" "sync" + "go.uber.org/zap" "golang.org/x/sync/singleflight" "github.com/spacemeshos/go-spacemesh/p2p" @@ -19,8 +20,9 @@ import ( // has not been yet received and validated. type SetSyncBase struct { mtx sync.Mutex + logger *zap.Logger ps PairwiseSyncer - os OrderedSet + os rangesync.OrderedSet handler SyncKeyHandler waiting []<-chan singleflight.Result g singleflight.Group @@ -29,8 +31,14 @@ type SetSyncBase struct { var _ SyncBase = &SetSyncBase{} // NewSetSyncBase creates a new SetSyncBase. -func NewSetSyncBase(ps PairwiseSyncer, os OrderedSet, handler SyncKeyHandler) *SetSyncBase { +func NewSetSyncBase( + logger *zap.Logger, + ps PairwiseSyncer, + os rangesync.OrderedSet, + handler SyncKeyHandler, +) *SetSyncBase { return &SetSyncBase{ + logger: logger, ps: ps, os: os, handler: handler, @@ -53,7 +61,7 @@ func (ssb *SetSyncBase) Count() (int, error) { } info, err := ssb.os.GetRangeInfo(x, x) if err != nil { - return 0, err + return 0, fmt.Errorf("get range info: %w", err) } return info.Count, nil } @@ -64,7 +72,7 @@ func (ssb *SetSyncBase) Derive(p p2p.Peer) PeerSyncer { defer ssb.mtx.Unlock() return &peerSetSyncer{ SetSyncBase: ssb, - OrderedSet: ssb.os.Copy(true).(OrderedSet), + OrderedSet: ssb.os.Copy(true), p: p, handler: ssb.handler, } @@ -76,13 +84,13 @@ func (ssb *SetSyncBase) Probe(ctx context.Context, p p2p.Peer) (rangesync.ProbeR ssb.mtx.Lock() os := ssb.os.Copy(true) ssb.mtx.Unlock() - defer os.(OrderedSet).Release() pr, err := ssb.ps.Probe(ctx, p, os, nil, nil) if err != nil { - return rangesync.ProbeResult{}, err + os.Release() + return rangesync.ProbeResult{}, fmt.Errorf("probing peer %s: %w", p, err) } - return pr, os.(OrderedSet).Release() + return pr, os.Release() } func (ssb *SetSyncBase) receiveKey(k rangesync.KeyBytes, p p2p.Peer) error { @@ -118,13 +126,20 @@ func (ssb *SetSyncBase) Wait() error { waiting := ssb.waiting ssb.waiting = nil ssb.mtx.Unlock() - var errs []error + gotError := false for _, w := range waiting { r := <-w - ssb.g.Forget(r.Val.(string)) - errs = append(errs, r.Err) + key := r.Val.(string) + ssb.g.Forget(key) + if r.Err != nil { + gotError = true + ssb.logger.Error("error from key handler", zap.String("key", key), zap.Error(r.Err)) + } } - return errors.Join(errs...) + if gotError { + return errors.New("some key handlers failed") + } + return nil } func (ssb *SetSyncBase) advance() error { @@ -135,14 +150,14 @@ func (ssb *SetSyncBase) advance() error { type peerSetSyncer struct { *SetSyncBase - OrderedSet + rangesync.OrderedSet p p2p.Peer handler SyncKeyHandler } var ( - _ PeerSyncer = &peerSetSyncer{} - _ OrderedSet = &peerSetSyncer{} + _ PeerSyncer = &peerSetSyncer{} + _ rangesync.OrderedSet = &peerSetSyncer{} ) // Peer implements Syncer. diff --git a/sync2/multipeer/setsyncbase_test.go b/sync2/multipeer/setsyncbase_test.go index 5a38ce2bfa..cc0e5e84b1 100644 --- a/sync2/multipeer/setsyncbase_test.go +++ b/sync2/multipeer/setsyncbase_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" gomock "go.uber.org/mock/gomock" + "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/p2p" @@ -28,7 +29,7 @@ type setSyncBaseTester struct { doneCh chan rangesync.KeyBytes } -func newSetSyncBaseTester(t *testing.T, os multipeer.OrderedSet) *setSyncBaseTester { +func newSetSyncBaseTester(t *testing.T, os rangesync.OrderedSet) *setSyncBaseTester { ctrl := gomock.NewController(t) st := &setSyncBaseTester{ T: t, @@ -51,7 +52,7 @@ func newSetSyncBaseTester(t *testing.T, os multipeer.OrderedSet) *setSyncBaseTes st.doneCh <- k return true, err }).AnyTimes() - st.ssb = multipeer.NewSetSyncBase(st.ps, os, st.handler) + st.ssb = multipeer.NewSetSyncBase(zaptest.NewLogger(t), st.ps, os, st.handler) return st } @@ -229,12 +230,11 @@ func TestSetSyncBase(t *testing.T) { st.handler.EXPECT().Commit(gomock.Any(), gomock.Any(), gomock.Any()) st.os.EXPECT().Advance() require.NoError(t, ss.Sync(context.Background(), nil, nil)) - handlerErr := errors.New("fail") - st.getWaitCh(k1) <- handlerErr + st.getWaitCh(k1) <- errors.New("fail") close(st.getWaitCh(k2)) handledKeys, err := st.wait(2) - require.ErrorIs(t, err, handlerErr) + require.ErrorContains(t, err, "some key handlers failed") require.ElementsMatch(t, []rangesync.KeyBytes{k1, k2}, handledKeys) }) @@ -244,27 +244,26 @@ func TestSetSyncBase(t *testing.T) { for n := range hs { hs[n] = rangesync.RandomKeyBytes(32) } - os := multipeer.NewDumbHashSet() + var os rangesync.DumbSet os.AddUnchecked(hs[0]) os.AddUnchecked(hs[1]) - st := newSetSyncBaseTester(t, os) + st := newSetSyncBaseTester(t, &os) ss := st.ssb.Derive(p2p.Peer("p1")) ss.(rangesync.OrderedSet).Receive(hs[2]) ss.(rangesync.OrderedSet).Add(hs[2]) ss.(rangesync.OrderedSet).Receive(hs[3]) ss.(rangesync.OrderedSet).Add(hs[3]) - // syncer's cloned ItemStore has new key immediately - has, err := ss.(multipeer.OrderedSet).Has(hs[2]) + // syncer's cloned set has new key immediately + has, err := ss.(rangesync.OrderedSet).Has(hs[2]) require.NoError(t, err) require.True(t, has) - has, err = ss.(multipeer.OrderedSet).Has(hs[3]) + has, err = ss.(rangesync.OrderedSet).Has(hs[3]) require.NoError(t, err) require.True(t, has) - handlerErr := errors.New("fail") - st.getWaitCh(hs[2]) <- handlerErr + st.getWaitCh(hs[2]) <- errors.New("fail") close(st.getWaitCh(hs[3])) handledKeys, err := st.wait(2) - require.ErrorIs(t, err, handlerErr) + require.ErrorContains(t, err, "some key handlers failed") require.ElementsMatch(t, hs[2:], handledKeys) // only successfully handled keys propagate the syncBase received, err := os.Received().Collect() diff --git a/sync2/multipeer/split_sync.go b/sync2/multipeer/split_sync.go index dfec97945e..7bfbe8394b 100644 --- a/sync2/multipeer/split_sync.go +++ b/sync2/multipeer/split_sync.go @@ -55,21 +55,19 @@ func newSplitSync( panic("BUG: no peers passed to splitSync") } return &splitSync{ - logger: logger, - syncBase: syncBase, - peers: peers, - syncPeers: syncPeers, - gracePeriod: gracePeriod, - clock: clock, - sq: newSyncQueue(len(syncPeers), keyLen, maxDepth), - // TODO: should not need buffering (stop when finished) - resCh: make(chan syncResult, 3*len(syncPeers)), + logger: logger, + syncBase: syncBase, + peers: peers, + syncPeers: syncPeers, + gracePeriod: gracePeriod, + clock: clock, + sq: newSyncQueue(len(syncPeers), keyLen, maxDepth), + resCh: make(chan syncResult), syncMap: make(map[p2p.Peer]*syncRange), failedPeers: make(map[p2p.Peer]struct{}), numRemaining: len(syncPeers), numPeers: len(syncPeers), - // TODO: should not need buffering (stop when finished) - slowRangeCh: make(chan *syncRange, 3*len(syncPeers)), + slowRangeCh: make(chan *syncRange), } } @@ -109,7 +107,11 @@ func (s *splitSync) startPeerSync(ctx context.Context, p p2p.Peer, sr *syncRange case <-gpTimer: // if another peer finishes its part early, let // it pick up this range - s.slowRangeCh <- sr + select { + case <-ctx.Done(): + return ctx.Err() + case s.slowRangeCh <- sr: + } } return nil }) @@ -174,14 +176,15 @@ func (s *splitSync) Sync(ctx context.Context) error { var sr *syncRange for { sr := s.sq.PopRange() - if sr != nil { - if sr.Done { - continue - } - p := s.nextPeer() - s.syncMap[p] = sr - s.startPeerSync(syncCtx, p, sr) + if sr == nil { + break + } + if sr.Done { + continue } + p := s.nextPeer() + s.syncMap[p] = sr + s.startPeerSync(syncCtx, p, sr) break } s.clearDeadPeers() diff --git a/sync2/p2p.go b/sync2/p2p.go index 5e7801a133..6886055593 100644 --- a/sync2/p2p.go +++ b/sync2/p2p.go @@ -22,27 +22,18 @@ type Dispatcher = rangesync.Dispatcher // Config contains the configuration for the P2PHashSync. type Config struct { + rangesync.RangeSetReconcilerConfig `mapstructure:",squash"` multipeer.MultiPeerReconcilerConfig `mapstructure:",squash"` - MaxSendRange int `mapstructure:"max-send-range"` - SampleSize int `mapstructure:"sample-size"` - RecentTimeSpan time.Duration `mapstructure:"recent-time-span"` - EnableActiveSync bool `mapstructure:"enable-active-sync"` - MaxReconcDiff float64 `mapstructure:"max-reconc-diff"` - AutoCommitCount int `mapstructure:"auto-commit-count"` - AutoCommitIdle time.Duration `mapstructure:"auto-commit-idle"` - TrafficLimit int `mapstructure:"traffic-limit"` - MessageLimit int `mapstructure:"message-limit"` + EnableActiveSync bool `mapstructure:"enable-active-sync"` + TrafficLimit int `mapstructure:"traffic-limit"` + MessageLimit int `mapstructure:"message-limit"` } // DefaultConfig returns the default configuration for the P2PHashSync. func DefaultConfig() Config { return Config{ + RangeSetReconcilerConfig: rangesync.DefaultConfig(), MultiPeerReconcilerConfig: multipeer.DefaultConfig(), - MaxSendRange: rangesync.DefaultMaxSendRange, - SampleSize: rangesync.DefaultSampleSize, - MaxReconcDiff: 0.01, - AutoCommitCount: 10000, - AutoCommitIdle: time.Second, TrafficLimit: 200_000_000, MessageLimit: 20_000_000, } @@ -52,7 +43,7 @@ func DefaultConfig() Config { type P2PHashSync struct { logger *zap.Logger cfg Config - os multipeer.OrderedSet + os rangesync.OrderedSet syncBase multipeer.SyncBase reconciler *multipeer.MultiPeerReconciler cancel context.CancelFunc @@ -66,7 +57,7 @@ func NewP2PHashSync( logger *zap.Logger, d *Dispatcher, name string, - os multipeer.OrderedSet, + os rangesync.OrderedSet, keyLen, maxDepth int, peers *peers.Peers, handler multipeer.SyncKeyHandler, @@ -78,21 +69,9 @@ func NewP2PHashSync( os: os, cfg: cfg, } - rangeSyncOpts := []rangesync.RangeSetReconcilerOption{ - rangesync.WithMaxSendRange(cfg.MaxSendRange), - rangesync.WithSampleSize(cfg.SampleSize), - rangesync.WithMaxDiff(cfg.MaxReconcDiff), - rangesync.WithLogger(logger), - } - if cfg.RecentTimeSpan > 0 { - rangeSyncOpts = append(rangeSyncOpts, rangesync.WithRecentTimeSpan(cfg.RecentTimeSpan)) - } // var ps multipeer.PairwiseSyncer - ps := rangesync.NewPairwiseSetSyncer(requester, name, rangeSyncOpts, []rangesync.ConduitOption{ - rangesync.WithTrafficLimit(cfg.TrafficLimit), - rangesync.WithMessageLimit(cfg.MessageLimit), - }) - s.syncBase = multipeer.NewSetSyncBase(ps, s.os, handler) + ps := rangesync.NewPairwiseSetSyncer(logger, requester, name, cfg.RangeSetReconcilerConfig) + s.syncBase = multipeer.NewSetSyncBase(logger, ps, s.os, handler) s.reconciler = multipeer.NewMultiPeerReconciler( logger, cfg.MultiPeerReconcilerConfig, s.syncBase, peers, keyLen, maxDepth) diff --git a/sync2/p2p_test.go b/sync2/p2p_test.go index 5116302400..a36e4ebf27 100644 --- a/sync2/p2p_test.go +++ b/sync2/p2p_test.go @@ -16,7 +16,6 @@ import ( "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/server" "github.com/spacemeshos/go-spacemesh/sync2" - "github.com/spacemeshos/go-spacemesh/sync2/multipeer" "github.com/spacemeshos/go-spacemesh/sync2/rangesync" ) @@ -47,7 +46,7 @@ func (fh *fakeHandler) Receive(k rangesync.KeyBytes, peer p2p.Peer) (bool, error return true, nil } -func (fh *fakeHandler) Commit(peer p2p.Peer, base, new multipeer.OrderedSet) error { +func (fh *fakeHandler) Commit(peer p2p.Peer, base, new rangesync.OrderedSet) error { fh.mtx.Lock() defer fh.mtx.Unlock() for k := range fh.synced { @@ -102,7 +101,7 @@ func TestP2P(t *testing.T) { synced: make(map[addedKey]struct{}), committed: make(map[string]struct{}), } - os := multipeer.NewDumbHashSet() + var os rangesync.DumbSet d := rangesync.NewDispatcher(logger) srv := d.SetupServer(host, "sync2test", server.WithLog(logger)) ctx, cancel := context.WithCancel(context.Background()) @@ -110,9 +109,9 @@ func TestP2P(t *testing.T) { eg.Go(func() error { return srv.Run(ctx) }) hs[n] = sync2.NewP2PHashSync( logger.Named(fmt.Sprintf("node%d", n)), - d, "test", os, keyLen, maxDepth, ps, handlers[n], cfg, srv) + d, "test", &os, keyLen, maxDepth, ps, handlers[n], cfg, srv) require.NoError(t, hs[n].Load()) - is := hs[n].Set().(*multipeer.DumbSet) + is := hs[n].Set().(*rangesync.DumbSet) is.SetAllowMultiReceive(true) if n == 0 { for _, h := range initialSet { @@ -131,7 +130,7 @@ func TestP2P(t *testing.T) { } os := hsync.Set().Copy(false) for _, k := range handlers[n].committedItems() { - os.(*multipeer.DumbSet).AddUnchecked(k) + os.(*rangesync.DumbSet).AddUnchecked(k) } empty, err := os.Empty() require.NoError(t, err) @@ -153,7 +152,7 @@ func TestP2P(t *testing.T) { hsync.Stop() os := hsync.Set().Copy(false) for _, k := range handlers[n].committedItems() { - os.(*multipeer.DumbSet).AddUnchecked(k) + os.(*rangesync.DumbSet).AddUnchecked(k) } actualItems, err := os.Items().Collect() require.NoError(t, err) diff --git a/sync2/rangesync/dispatcher_test.go b/sync2/rangesync/dispatcher_test.go index efdeb878e1..3b4cb49753 100644 --- a/sync2/rangesync/dispatcher_test.go +++ b/sync2/rangesync/dispatcher_test.go @@ -18,7 +18,7 @@ import ( func makeFakeDispHandler(n int) rangesync.Handler { return func(ctx context.Context, stream io.ReadWriter) error { x := rangesync.KeyBytes(bytes.Repeat([]byte{byte(n)}, 32)) - c := rangesync.StartWireConduit(ctx, stream) + c := rangesync.StartWireConduit(ctx, stream, rangesync.DefaultConfig()) defer c.End() s := rangesync.Sender{c} s.SendRangeContents(x, x, n) @@ -59,7 +59,7 @@ func TestDispatcher(t *testing.T) { require.NoError(t, c.StreamRequest( context.Background(), srvPeerID, []byte(tt.name), func(ctx context.Context, stream io.ReadWriter) error { - c := rangesync.StartWireConduit(ctx, stream) + c := rangesync.StartWireConduit(ctx, stream, rangesync.DefaultConfig()) defer c.End() m, err := c.NextMessage() require.NoError(t, err) diff --git a/sync2/rangesync/export_test.go b/sync2/rangesync/export_test.go index b55abbfa27..4bd445d5ea 100644 --- a/sync2/rangesync/export_test.go +++ b/sync2/rangesync/export_test.go @@ -1,14 +1,17 @@ package rangesync var ( - StartWireConduit = startWireConduit - StringToFP = stringToFP - CHash = chash - NaiveFPFunc = naiveFPFunc + StartWireConduit = startWireConduit + StringToFP = stringToFP + CHash = chash + NaiveFPFunc = naiveFPFunc + NewRangeSetReconcilerInternal = newRangeSetReconciler + NewPairwiseSetSyncerInternal = newPairwiseSetSyncer ) type ( - Sender = sender + Sender = sender + NullTracer = nullTracer ) func (rsr *RangeSetReconciler) DoRound(s Sender) (done bool, err error) { return rsr.doRound(s) } diff --git a/sync2/rangesync/p2p.go b/sync2/rangesync/p2p.go index e3f70cba41..dff29abbba 100644 --- a/sync2/rangesync/p2p.go +++ b/sync2/rangesync/p2p.go @@ -6,50 +6,71 @@ import ( "io" "sync/atomic" + "github.com/jonboulle/clockwork" + "go.uber.org/zap" + "github.com/spacemeshos/go-spacemesh/p2p" ) type PairwiseSetSyncer struct { - r Requester - name string - opts []RangeSetReconcilerOption - conduitOpts []ConduitOption - sent atomic.Int64 - recv atomic.Int64 + logger *zap.Logger + r Requester + name string + cfg RangeSetReconcilerConfig + sent atomic.Int64 + recv atomic.Int64 + tracer Tracer + clock clockwork.Clock } -func NewPairwiseSetSyncer( +func newPairwiseSetSyncer( + logger *zap.Logger, r Requester, name string, - opts []RangeSetReconcilerOption, - conduitOpts []ConduitOption, + cfg RangeSetReconcilerConfig, + tracer Tracer, + clock clockwork.Clock, ) *PairwiseSetSyncer { return &PairwiseSetSyncer{ - r: r, - name: name, - opts: opts, - conduitOpts: conduitOpts, + logger: logger, + r: r, + name: name, + cfg: cfg, + tracer: tracer, + clock: clock, } } +func NewPairwiseSetSyncer( + logger *zap.Logger, + r Requester, + name string, + cfg RangeSetReconcilerConfig, +) *PairwiseSetSyncer { + return newPairwiseSetSyncer(logger, r, name, cfg, nullTracer{}, clockwork.NewRealClock()) +} + func (pss *PairwiseSetSyncer) updateCounts(c *wireConduit) { pss.sent.Add(int64(c.bytesSent())) pss.recv.Add(int64(c.bytesReceived())) } +func (pss *PairwiseSetSyncer) createReconciler(os OrderedSet) *RangeSetReconciler { + return newRangeSetReconciler(pss.logger, pss.cfg, os, pss.tracer, pss.clock) +} + func (pss *PairwiseSetSyncer) Probe( ctx context.Context, peer p2p.Peer, os OrderedSet, x, y KeyBytes, -) (ProbeResult, error) { - var pr ProbeResult - rsr := NewRangeSetReconciler(os, pss.opts...) +) (pr ProbeResult, err error) { + rsr := pss.createReconciler(os) initReq := []byte(pss.name) - if err := pss.r.StreamRequest( + if err = pss.r.StreamRequest( ctx, peer, initReq, func(ctx context.Context, stream io.ReadWriter) (err error) { - c := startWireConduit(ctx, stream, pss.conduitOpts...) + c := startWireConduit(ctx, stream, pss.cfg) defer func() { // If the conduit is not closed by this point, stop it // interrupting any ongoing send operations @@ -79,7 +100,7 @@ func (pss *PairwiseSetSyncer) requestCallback( rsr *RangeSetReconciler, x, y KeyBytes, ) error { - c := startWireConduit(ctx, stream, pss.conduitOpts...) + c := startWireConduit(ctx, stream, pss.cfg) defer func() { c.Stop() pss.updateCounts(c) @@ -100,7 +121,7 @@ func (pss *PairwiseSetSyncer) Sync( os OrderedSet, x, y KeyBytes, ) error { - rsr := NewRangeSetReconciler(os, pss.opts...) + rsr := pss.createReconciler(os) initReq := []byte(pss.name) return pss.r.StreamRequest( ctx, peer, initReq, @@ -110,9 +131,9 @@ func (pss *PairwiseSetSyncer) Sync( } func (pss *PairwiseSetSyncer) Serve(ctx context.Context, stream io.ReadWriter, os OrderedSet) error { - c := startWireConduit(ctx, stream, pss.conduitOpts...) + c := startWireConduit(ctx, stream, pss.cfg) defer c.Stop() - rsr := NewRangeSetReconciler(os, pss.opts...) + rsr := pss.createReconciler(os) if err := rsr.Run(c); err != nil { return err } diff --git a/sync2/rangesync/p2p_test.go b/sync2/rangesync/p2p_test.go index 022566a2ba..cf149bb12d 100644 --- a/sync2/rangesync/p2p_test.go +++ b/sync2/rangesync/p2p_test.go @@ -10,6 +10,7 @@ import ( "github.com/jonboulle/clockwork" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/stretchr/testify/require" + "go.uber.org/zap" "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/p2p" @@ -36,15 +37,18 @@ func newClientServerTester( tb testing.TB, set rangesync.OrderedSet, getRequester getRequesterFunc, - opts []rangesync.RangeSetReconcilerOption, - conduitOpts []rangesync.ConduitOption, + cfg rangesync.RangeSetReconcilerConfig, + tracer rangesync.Tracer, + clock clockwork.Clock, ) (*clientServerTester, context.Context) { var ( cst clientServerTester srv rangesync.Requester ) d := rangesync.NewDispatcher(zaptest.NewLogger(tb)) - cst.pss = rangesync.NewPairwiseSetSyncer(nil, "test", opts, conduitOpts) + logger := zap.NewNop() + cst.pss = rangesync.NewPairwiseSetSyncerInternal( + logger, nil, "test", cfg, tracer, clock) cst.pss.Register(d, set) srv, cst.srvPeerID = getRequester("srv", d.Dispatch) ctx := runRequester(tb, srv) @@ -168,7 +172,7 @@ func testWireSync(t *testing.T, getRequester getRequesterFunc) { name string cfg hashSyncTestConfig dumb bool - opts []rangesync.RangeSetReconcilerOption + rCfg func(*rangesync.RangeSetReconcilerConfig) advance time.Duration sentRecent bool receivedRecent bool @@ -208,8 +212,8 @@ func testWireSync(t *testing.T, getRequester getRequesterFunc) { maxNumSpecificB: 500, }, dumb: false, - opts: []rangesync.RangeSetReconcilerOption{ - rangesync.WithRecentTimeSpan(990 * time.Second), + rCfg: func(cfg *rangesync.RangeSetReconcilerConfig) { + cfg.RecentTimeSpan = 990 * time.Second }, advance: 1000 * time.Second, sentRecent: true, @@ -239,12 +243,15 @@ func testWireSync(t *testing.T, getRequester getRequesterFunc) { setB := &fakeRecentSet{OrderedSet: st.setB, clock: clock} require.NoError(t, setB.registerAll(context.Background())) var tr syncTracer - opts := append(st.opts, rangesync.WithTracer(&tr), rangesync.WithClock(clock)) - opts = append(opts, tc.opts...) - opts = opts[0:len(opts):len(opts)] clock.Advance(tc.advance) - cst, ctx := newClientServerTester(t, setA, getRequester, opts, nil) - pss := rangesync.NewPairwiseSetSyncer(cst.client, "test", opts, nil) + cfg := st.cfg + if tc.rCfg != nil { + tc.rCfg(&cfg) + } + cst, ctx := newClientServerTester(t, setA, getRequester, cfg, &tr, clock) + logger := zap.NewNop() + pss := rangesync.NewPairwiseSetSyncerInternal( + logger, cst.client, "test", cfg, &tr, clock) err := pss.Sync(ctx, cst.srvPeerID, setB, nil, nil) require.NoError(t, err) st.setA.AddReceived() @@ -252,7 +259,7 @@ func testWireSync(t *testing.T, getRequester getRequesterFunc) { t.Logf("numSpecific: %d, bytesSent %d, bytesReceived %d", st.numSpecificA+st.numSpecificB, - cst.pss.Sent(), cst.pss.Received()) + pss.Sent(), pss.Received()) require.Equal(t, tc.dumb, tr.dumb.Load(), "dumb sync") require.Equal(t, tc.receivedRecent, tr.receivedItems > 0) require.Equal(t, tc.sentRecent, tr.sentItems > 0) @@ -279,8 +286,11 @@ func testWireProbe(t *testing.T, getRequester getRequesterFunc) { minNumSpecificB: 130, maxNumSpecificB: 130, }) - cst, ctx := newClientServerTester(t, st.setA, getRequester, st.opts, nil) - pss := rangesync.NewPairwiseSetSyncer(cst.client, "test", st.opts, nil) + var tr rangesync.NullTracer + clock := clockwork.NewRealClock() + logger := zap.NewNop() + cst, ctx := newClientServerTester(t, st.setA, getRequester, st.cfg, &tr, clock) + pss := rangesync.NewPairwiseSetSyncerInternal(logger, cst.client, "test", st.cfg, &tr, clock) itemsA := st.setA.Items() x, err := itemsA.First() require.NoError(t, err) @@ -312,42 +322,40 @@ func TestWireProbe(t *testing.T) { func TestPairwiseSyncerLimits(t *testing.T) { for _, tc := range []struct { - name string - clientConduitOpts []rangesync.ConduitOption - serverConduitOpts []rangesync.ConduitOption - error bool + name string + clientTrafficLimit int + clientMessageLimit int + serverTrafficLimit int + serverMessageLimit int + error bool }{ { - name: "client traffic limit hit", - clientConduitOpts: []rangesync.ConduitOption{rangesync.WithTrafficLimit(100)}, - error: true, + name: "client traffic limit hit", + clientTrafficLimit: 100, + error: true, }, { - name: "client message limit hit", - clientConduitOpts: []rangesync.ConduitOption{rangesync.WithTrafficLimit(10)}, - error: true, + name: "client message limit hit", + clientMessageLimit: 10, + error: true, }, { - name: "server traffic limit hit", - serverConduitOpts: []rangesync.ConduitOption{rangesync.WithTrafficLimit(100)}, - error: true, + name: "server traffic limit hit", + serverTrafficLimit: 100, + error: true, }, { - name: "server message limit hit", - serverConduitOpts: []rangesync.ConduitOption{rangesync.WithTrafficLimit(10)}, - error: true, + name: "server message limit hit", + serverMessageLimit: 10, + error: true, }, { - name: "reasonable limits", - clientConduitOpts: []rangesync.ConduitOption{ - rangesync.WithTrafficLimit(100_000), - rangesync.WithMessageLimit(1000), - }, - serverConduitOpts: []rangesync.ConduitOption{ - rangesync.WithTrafficLimit(100_000), - rangesync.WithMessageLimit(1000), - }, - error: false, + name: "reasonable limits", + clientTrafficLimit: 100_000, + clientMessageLimit: 1000, + serverTrafficLimit: 100_000, + serverMessageLimit: 1000, + error: false, }, } { t.Run(tc.name, func(t *testing.T) { @@ -361,12 +369,18 @@ func TestPairwiseSyncerLimits(t *testing.T) { }) clock := clockwork.NewFakeClockAt(startDate) var tr syncTracer - opts := append(st.opts, rangesync.WithTracer(&tr), rangesync.WithClock(clock)) - opts = opts[0:len(opts):len(opts)] + srvCfg := st.cfg + srvCfg.TrafficLimit = tc.serverTrafficLimit + srvCfg.MessageLimit = tc.serverMessageLimit cst, ctx := newClientServerTester( - t, st.setA, p2pRequesterGetter(t), opts, - tc.serverConduitOpts) - pss := rangesync.NewPairwiseSetSyncer(cst.client, "test", opts, tc.clientConduitOpts) + t, st.setA, p2pRequesterGetter(t), + srvCfg, &tr, clock) + logger := zap.NewNop() + clientCfg := st.cfg + clientCfg.TrafficLimit = tc.clientTrafficLimit + clientCfg.MessageLimit = tc.clientMessageLimit + pss := rangesync.NewPairwiseSetSyncerInternal( + logger, cst.client, "test", clientCfg, &tr, clock) err := pss.Sync(ctx, cst.srvPeerID, st.setB, nil, nil) if tc.error { require.Error(t, err) diff --git a/sync2/rangesync/rangesync.go b/sync2/rangesync/rangesync.go index 313e461949..07ff1305dc 100644 --- a/sync2/rangesync/rangesync.go +++ b/sync2/rangesync/rangesync.go @@ -18,52 +18,33 @@ const ( maxSampleSize = 1000 ) -// RangeSetReconcilerOption is a configuration option for RangeSetReconciler. -type RangeSetReconcilerOption func(r *RangeSetReconciler) - -// WithMaxSendRange sets the maximum range size to send instead of further subdividing the -// input range. -func WithMaxSendRange(n int) RangeSetReconcilerOption { - return func(r *RangeSetReconciler) { - r.maxSendRange = n - } +type RangeSetReconcilerConfig struct { + // Maximum range size to send instead of further subdividing the input range. + MaxSendRange int `mapstructure:"max-send-range"` + // Size of the item chunk to use when sending the set items. + ItemChunkSize int `mapstructure:"item-chunk-size"` + // Size of the MinHash sample to be sent to the peer. + SampleSize int `mapstructure:"sample-size"` + // Maximum set difference metric (0..1) allowed for recursive reconciliation, with + // value of 0 meaning equal sets and 1 meaning completely disjoint set. If the + // difference metric MaxReconcDiff value, the whole set is transmitted instead of + // applying the recursive algorithm. + MaxReconcDiff float64 `mapstructure:"max-reconc-diff"` + // Time span for recent sync. + RecentTimeSpan time.Duration `mapstructure:"recent-time-span"` + TrafficLimit int + MessageLimit int } -// WithItemChunkSize sets the size of the item chunk to use when sending the set items. -func WithItemChunkSize(n int) RangeSetReconcilerOption { - return func(r *RangeSetReconciler) { - r.itemChunkSize = n - } -} - -// WithSampleSize sets the size of the MinHash sample to be sent to the peer. -func WithSampleSize(s int) RangeSetReconcilerOption { - return func(r *RangeSetReconciler) { - r.sampleSize = s - } -} - -// WithMaxDiff sets maximum set difference metric (0..1) allowed for recursive -// reconciliation, with value of 0 meaning equal sets and 1 meaning completely disjoint -// set. If the difference metric MaxDiff value, the whole set is transmitted instead of -// applying the recursive algorithm. -func WithMaxDiff(d float64) RangeSetReconcilerOption { - return func(r *RangeSetReconciler) { - r.maxDiff = d - } -} - -// WithLogger specifies the logger for RangeSetReconciler. -func WithLogger(log *zap.Logger) RangeSetReconcilerOption { - return func(r *RangeSetReconciler) { - r.log = log - } -} - -// WithRecentTimeSpan specifies the time span for recent items. -func WithRecentTimeSpan(d time.Duration) RangeSetReconcilerOption { - return func(r *RangeSetReconciler) { - r.recentTimeSpan = d +// DefaultConfig returns the default configuration for the RangeSetReconciler. +func DefaultConfig() RangeSetReconcilerConfig { + return RangeSetReconcilerConfig{ + MaxSendRange: DefaultMaxSendRange, + ItemChunkSize: DefaultItemChunkSize, + SampleSize: DefaultSampleSize, + MaxReconcDiff: 0.01, + TrafficLimit: 200_000_000, + MessageLimit: 20_000_000, } } @@ -81,20 +62,6 @@ type nullTracer struct{} func (t nullTracer) OnDumbSync() {} func (t nullTracer) OnRecent(int, int) {} -// WithTracer specifies a tracer for RangeSetReconciler. -func WithTracer(t Tracer) RangeSetReconcilerOption { - return func(r *RangeSetReconciler) { - r.tracer = t - } -} - -// WithClock specifies the clock for RangeSetReconciler. -func WithClock(c clockwork.Clock) RangeSetReconcilerOption { - return func(r *RangeSetReconciler) { - r.clock = c - } -} - // ProbeResult contains the result of a probe. type ProbeResult struct { // Fingerprint of the range. @@ -109,38 +76,38 @@ type ProbeResult struct { // RangeSetReconciler reconciles two sets of items using the recursive set reconciliation // protocol. type RangeSetReconciler struct { - os OrderedSet - maxSendRange int - itemChunkSize int - sampleSize int - maxDiff float64 - recentTimeSpan time.Duration - tracer Tracer - clock clockwork.Clock - log *zap.Logger + os OrderedSet + cfg RangeSetReconcilerConfig + tracer Tracer + clock clockwork.Clock + logger *zap.Logger } -// NewRangeSetReconciler creates a new RangeSetReconciler. -func NewRangeSetReconciler(os OrderedSet, opts ...RangeSetReconcilerOption) *RangeSetReconciler { +func newRangeSetReconciler( + logger *zap.Logger, + cfg RangeSetReconcilerConfig, + os OrderedSet, + tracer Tracer, + clock clockwork.Clock, +) *RangeSetReconciler { rsr := &RangeSetReconciler{ - os: os, - maxSendRange: DefaultMaxSendRange, - itemChunkSize: DefaultItemChunkSize, - sampleSize: DefaultSampleSize, - maxDiff: -1, - tracer: nullTracer{}, - clock: clockwork.NewRealClock(), - log: zap.NewNop(), - } - for _, opt := range opts { - opt(rsr) - } - if rsr.maxSendRange <= 0 { + os: os, + cfg: cfg, + tracer: tracer, + clock: clock, + logger: logger, + } + if rsr.cfg.MaxSendRange <= 0 { panic("bad maxSendRange") } return rsr } +// NewRangeSetReconciler creates a new RangeSetReconciler. +func NewRangeSetReconciler(logger *zap.Logger, cfg RangeSetReconcilerConfig, os OrderedSet) *RangeSetReconciler { + return newRangeSetReconciler(logger, cfg, os, nullTracer{}, clockwork.NewRealClock()) +} + func (rsr *RangeSetReconciler) defaultRange() (x, y KeyBytes, err error) { if empty, err := rsr.os.Empty(); err != nil { return nil, nil, fmt.Errorf("checking for empty set: %w", err) @@ -157,13 +124,13 @@ func (rsr *RangeSetReconciler) defaultRange() (x, y KeyBytes, err error) { } func (rsr *RangeSetReconciler) processSubrange(s sender, x, y KeyBytes, info RangeInfo) error { - rsr.log.Debug("processSubrange", log.ZShortStringer("x", x), log.ZShortStringer("y", y), + rsr.logger.Debug("processSubrange", log.ZShortStringer("x", x), log.ZShortStringer("y", y), zap.Int("count", info.Count), log.ZShortStringer("fingerprint", info.Fingerprint)) if info.Count == 0 { // We have no more items in this subrange. // Ask peer to send any items it has in the range - rsr.log.Debug("processSubrange: send empty range", log.ZShortStringer("x", x), log.ZShortStringer("y", y)) + rsr.logger.Debug("processSubrange: send empty range", log.ZShortStringer("x", x), log.ZShortStringer("y", y)) if err := s.SendEmptyRange(x, y); err != nil { return fmt.Errorf("send empty range: %w", err) } @@ -171,7 +138,7 @@ func (rsr *RangeSetReconciler) processSubrange(s sender, x, y KeyBytes, info Ran // The range is non-empty and large enough. // Send fingerprint so that the peer can further subdivide it. - rsr.log.Debug("processSubrange: send fingerprint", log.ZShortStringer("x", x), log.ZShortStringer("y", y), + rsr.logger.Debug("processSubrange: send fingerprint", log.ZShortStringer("x", x), log.ZShortStringer("y", y), zap.Int("count", info.Count)) if err := s.SendFingerprint(x, y, info.Fingerprint, info.Count); err != nil { return fmt.Errorf("send fingerprint: %w", err) @@ -182,14 +149,14 @@ func (rsr *RangeSetReconciler) processSubrange(s sender, x, y KeyBytes, info Ran func (rsr *RangeSetReconciler) splitRange(s sender, count int, x, y KeyBytes) error { count = count / 2 - rsr.log.Debug("handleMessage: PRE split range", + rsr.logger.Debug("handleMessage: PRE split range", log.ZShortStringer("x", x), log.ZShortStringer("y", y), zap.Int("countArg", count)) si, err := rsr.os.SplitRange(x, y, count) if err != nil { return fmt.Errorf("split range: %w", err) } - rsr.log.Debug("handleMessage: split range", + rsr.logger.Debug("handleMessage: split range", log.ZShortStringer("x", x), log.ZShortStringer("y", y), zap.Int("countArg", count), zap.Int("count0", si.Parts[0].Count), @@ -214,14 +181,14 @@ func (rsr *RangeSetReconciler) sendSmallRange( x, y KeyBytes, ) error { if count == 0 { - rsr.log.Debug("handleMessage: empty incoming range", + rsr.logger.Debug("handleMessage: empty incoming range", log.ZShortStringer("x", x), log.ZShortStringer("y", y)) return s.SendEmptyRange(x, y) } - rsr.log.Debug("handleMessage: send small range", + rsr.logger.Debug("handleMessage: send small range", log.ZShortStringer("x", x), log.ZShortStringer("y", y), zap.Int("count", count), - zap.Int("maxSendRange", rsr.maxSendRange)) + zap.Int("maxSendRange", rsr.cfg.MaxSendRange)) if _, err := rsr.sendItems(s, count, sr, nil); err != nil { return fmt.Errorf("send items: %w", err) } @@ -238,14 +205,14 @@ func (rsr *RangeSetReconciler) sendItems( return 0, nil } nSent := 0 - if rsr.itemChunkSize == 0 { + if rsr.cfg.ItemChunkSize == 0 { panic("BUG: zero item chunk size") } var keys []KeyBytes n := count for k := range sr.Seq { if _, found := skipKeys[string(k)]; !found { - if len(keys) == rsr.itemChunkSize { + if len(keys) == rsr.cfg.ItemChunkSize { if err := s.SendChunk(keys); err != nil { return nSent, err } @@ -283,34 +250,34 @@ func (rsr *RangeSetReconciler) handleFingerprint( // The range is synced return true, nil - case msg.Type() == MessageTypeSample && rsr.maxDiff >= 0: + case msg.Type() == MessageTypeSample && rsr.cfg.MaxReconcDiff >= 0: // The peer has sent a sample of its items in the range to check if // recursive reconciliation approach is feasible. pr, err := rsr.handleSample(msg, info) if err != nil { return false, err } - if 1-pr.Sim > rsr.maxDiff { + if 1-pr.Sim > rsr.cfg.MaxReconcDiff { rsr.tracer.OnDumbSync() - rsr.log.Debug("handleMessage: maxDiff exceeded, sending full range", + rsr.logger.Debug("handleMessage: maxDiff exceeded, sending full range", zap.Float64("sim", pr.Sim), zap.Float64("diff", 1-pr.Sim), - zap.Float64("maxDiff", rsr.maxDiff)) + zap.Float64("maxDiff", rsr.cfg.MaxReconcDiff)) if _, err := rsr.sendItems(s, info.Count, info.Items, nil); err != nil { return false, fmt.Errorf("send items: %w", err) } return false, s.SendRangeContents(x, y, info.Count) } - rsr.log.Debug("handleMessage: acceptable maxDiff, proceeding with sync", + rsr.logger.Debug("handleMessage: acceptable maxDiff, proceeding with sync", zap.Float64("sim", pr.Sim), zap.Float64("diff", 1-pr.Sim), - zap.Float64("maxDiff", rsr.maxDiff)) - if info.Count > rsr.maxSendRange { + zap.Float64("maxDiff", rsr.cfg.MaxReconcDiff)) + if info.Count > rsr.cfg.MaxSendRange { return false, rsr.splitRange(s, info.Count, x, y) } return false, rsr.sendSmallRange(s, info.Count, info.Items, x, y) - case info.Count <= rsr.maxSendRange: + case info.Count <= rsr.cfg.MaxSendRange: return false, rsr.sendSmallRange(s, info.Count, info.Items, x, y) default: @@ -373,7 +340,7 @@ func (rsr *RangeSetReconciler) handleRecent( if err := s.SendRecent(time.Time{}); err != nil { return fmt.Errorf("sending recent: %w", err) } - rsr.log.Debug("handled recent message", + rsr.logger.Debug("handled recent message", zap.Int("receivedCount", len(receivedKeys)), zap.Int("sentCount", nSent)) rsr.tracer.OnRecent(len(receivedKeys), nSent) @@ -387,7 +354,7 @@ func (rsr *RangeSetReconciler) handleMessage( msg SyncMessage, receivedKeys map[string]struct{}, ) (done bool, err error) { - rsr.log.Debug("handleMessage", zap.String("msg", SyncMessageToString(msg))) + rsr.logger.Debug("handleMessage", zap.String("msg", SyncMessageToString(msg))) x, y, err := rsr.messageRange(msg) if err != nil { @@ -409,7 +376,7 @@ func (rsr *RangeSetReconciler) handleMessage( if x == nil { switch msg.Type() { case MessageTypeProbe: - rsr.log.Debug("handleMessage: send empty probe response") + rsr.logger.Debug("handleMessage: send empty probe response") if err := s.SendSample( x, y, EmptyFingerprint(), 0, 0, EmptySeqResult(), ); err != nil { @@ -425,7 +392,7 @@ func (rsr *RangeSetReconciler) handleMessage( if err != nil { return false, err } - rsr.log.Debug("handleMessage: range info", + rsr.logger.Debug("handleMessage: range info", log.ZShortStringer("x", x), log.ZShortStringer("y", y), zap.Array("items", info.Items), zap.Int("count", info.Count), @@ -439,17 +406,17 @@ func (rsr *RangeSetReconciler) handleMessage( // side. In the latter case, send only the items themselves b/c // the range doesn't need any further handling by the peer. if info.Count != 0 { - rsr.log.Debug("handleMessage: send items", zap.Int("count", info.Count), + rsr.logger.Debug("handleMessage: send items", zap.Int("count", info.Count), zap.Array("items", info.Items), zap.Int("receivedCount", len(receivedKeys))) nSent, err := rsr.sendItems(s, info.Count, info.Items, receivedKeys) if err != nil { return false, fmt.Errorf("send items: %w", err) } - rsr.log.Debug("handleMessage: sent items", zap.Int("count", nSent)) + rsr.logger.Debug("handleMessage: sent items", zap.Int("count", nSent)) return false, nil } - rsr.log.Debug("handleMessage: local range is empty") + rsr.logger.Debug("handleMessage: local range is empty") return true, nil case MessageTypeProbe: @@ -496,7 +463,7 @@ func (rsr *RangeSetReconciler) Initiate(c Conduit, x, y KeyBytes) error { } else if x == nil || y == nil { panic("BUG: bad range") } - haveRecent := rsr.recentTimeSpan > 0 + haveRecent := rsr.cfg.RecentTimeSpan > 0 if err := rsr.initiate(s, x, y, haveRecent); err != nil { return err } @@ -504,9 +471,9 @@ func (rsr *RangeSetReconciler) Initiate(c Conduit, x, y KeyBytes) error { } func (rsr *RangeSetReconciler) initiate(s sender, x, y KeyBytes, haveRecent bool) error { - rsr.log.Debug("initiate", log.ZShortStringer("x", x), log.ZShortStringer("y", y)) + rsr.logger.Debug("initiate", log.ZShortStringer("x", x), log.ZShortStringer("y", y)) if x == nil { - rsr.log.Debug("initiate: send empty set") + rsr.logger.Debug("initiate: send empty set") return s.SendEmptySet() } info, err := rsr.os.GetRangeInfo(x, y) @@ -515,27 +482,27 @@ func (rsr *RangeSetReconciler) initiate(s sender, x, y KeyBytes, haveRecent bool } switch { case info.Count == 0: - rsr.log.Debug("initiate: send empty set") + rsr.logger.Debug("initiate: send empty set") return s.SendEmptyRange(x, y) - case info.Count < rsr.maxSendRange: - rsr.log.Debug("initiate: send whole range", zap.Int("count", info.Count)) + case info.Count < rsr.cfg.MaxSendRange: + rsr.logger.Debug("initiate: send whole range", zap.Int("count", info.Count)) if _, err := rsr.sendItems(s, info.Count, info.Items, nil); err != nil { return fmt.Errorf("send items: %w", err) } return s.SendRangeContents(x, y, info.Count) case haveRecent: - rsr.log.Debug("initiate: checking recent items") - since := rsr.clock.Now().Add(-rsr.recentTimeSpan) + rsr.logger.Debug("initiate: checking recent items") + since := rsr.clock.Now().Add(-rsr.cfg.RecentTimeSpan) items, count := rsr.os.Recent(since) if count != 0 { - rsr.log.Debug("initiate: sending recent items", zap.Int("count", count)) + rsr.logger.Debug("initiate: sending recent items", zap.Int("count", count)) if n, err := rsr.sendItems(s, count, items, nil); err != nil { return fmt.Errorf("send recent items: %w", err) } else if n != count { panic("BUG: wrong number of items sent") } } else { - rsr.log.Debug("initiate: no recent items") + rsr.logger.Debug("initiate: no recent items") } rsr.tracer.OnRecent(0, count) // Send Recent message even if there are no recent items, b/c we want to @@ -544,14 +511,14 @@ func (rsr *RangeSetReconciler) initiate(s sender, x, y KeyBytes, haveRecent bool return fmt.Errorf("send recent message: %w", err) } return nil - case rsr.maxDiff >= 0: + case rsr.cfg.MaxReconcDiff >= 0: // Use minhash to check if syncing this range is feasible - rsr.log.Debug("initiate: send sample", + rsr.logger.Debug("initiate: send sample", zap.Int("count", info.Count), - zap.Int("sampleSize", rsr.sampleSize)) - return s.SendSample(x, y, info.Fingerprint, info.Count, rsr.sampleSize, info.Items) + zap.Int("sampleSize", rsr.cfg.SampleSize)) + return s.SendSample(x, y, info.Fingerprint, info.Count, rsr.cfg.SampleSize, info.Items) default: - rsr.log.Debug("initiate: send fingerprint", zap.Int("count", info.Count)) + rsr.logger.Debug("initiate: send fingerprint", zap.Int("count", info.Count)) return s.SendFingerprint(x, y, info.Fingerprint, info.Count) } } @@ -567,7 +534,7 @@ func (rsr *RangeSetReconciler) InitiateProbe( if err != nil { return RangeInfo{}, err } - if err := s.SendProbe(x, y, info.Fingerprint, rsr.sampleSize); err != nil { + if err := s.SendProbe(x, y, info.Fingerprint, rsr.cfg.SampleSize); err != nil { return RangeInfo{}, err } if err := s.SendEndRound(); err != nil { @@ -585,7 +552,7 @@ func (rsr *RangeSetReconciler) handleSample( if info.Fingerprint == msg.Fingerprint() { pr.Sim = 1 } else { - localSample, err := Sample(info.Items, info.Count, rsr.sampleSize) + localSample, err := Sample(info.Items, info.Count, rsr.cfg.SampleSize) if err != nil { return ProbeResult{}, fmt.Errorf("sampling local items: %w", err) } @@ -702,8 +669,8 @@ RECV_LOOP: // Run performs sync reconciliation run using specified Conduit to send and receive // messages. func (rsr *RangeSetReconciler) Run(c Conduit) error { - rsr.log.Debug("begin set reconciliation") - defer rsr.log.Debug("end set reconciliation") + rsr.logger.Debug("begin set reconciliation") + defer rsr.logger.Debug("end set reconciliation") s := sender{c} for { // Process() will receive all items and messages from the peer diff --git a/sync2/rangesync/rangesync_test.go b/sync2/rangesync/rangesync_test.go index 203c845b83..6f54d002d6 100644 --- a/sync2/rangesync/rangesync_test.go +++ b/sync2/rangesync/rangesync_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "go.uber.org/zap" "go.uber.org/zap/zaptest" "golang.org/x/exp/maps" @@ -295,16 +296,13 @@ func TestRangeSync(t *testing.T) { logger := zaptest.NewLogger(t) for n, maxSendRange := range []int{1, 2, 3, 4} { t.Logf("maxSendRange: %d", maxSendRange) + cfg := rangesync.DefaultConfig() + cfg.MaxSendRange = maxSendRange + cfg.ItemChunkSize = 3 setA := makeSet(tc.a) - syncA := rangesync.NewRangeSetReconciler(setA, - rangesync.WithLogger(logger.Named("A")), - rangesync.WithMaxSendRange(maxSendRange), - rangesync.WithItemChunkSize(3)) + syncA := rangesync.NewRangeSetReconciler(logger.Named("A"), cfg, setA) setB := makeSet(tc.b) - syncB := rangesync.NewRangeSetReconciler(setB, - rangesync.WithLogger(logger.Named("B")), - rangesync.WithMaxSendRange(maxSendRange), - rangesync.WithItemChunkSize(3)) + syncB := rangesync.NewRangeSetReconciler(logger.Named("B"), cfg, setB) var ( nRounds int @@ -372,12 +370,12 @@ func TestRandomSync(t *testing.T) { slices.Sort(expectedSet) maxSendRange := rand.Intn(16) + 1 - syncA := rangesync.NewRangeSetReconciler(setA, - rangesync.WithMaxSendRange(maxSendRange), - rangesync.WithItemChunkSize(3)) - syncB := rangesync.NewRangeSetReconciler(setB, - rangesync.WithMaxSendRange(maxSendRange), - rangesync.WithItemChunkSize(3)) + cfg := rangesync.DefaultConfig() + cfg.MaxSendRange = maxSendRange + cfg.ItemChunkSize = 3 + logger := zap.NewNop() + syncA := rangesync.NewRangeSetReconciler(logger, cfg, setA) + syncB := rangesync.NewRangeSetReconciler(logger, cfg, setB) runSync(t, syncA, syncB, nil, nil, max(len(expectedSet), 2)) setA.AddReceived() @@ -401,20 +399,20 @@ type hashSyncTester struct { tb testing.TB src []rangesync.KeyBytes setA, setB *rangesync.DumbSet - opts []rangesync.RangeSetReconcilerOption + cfg rangesync.RangeSetReconcilerConfig numSpecificA int numSpecificB int } func newHashSyncTester(tb testing.TB, cfg hashSyncTestConfig) *hashSyncTester { tb.Helper() + rCfg := rangesync.DefaultConfig() + rCfg.MaxSendRange = cfg.maxSendRange + rCfg.MaxReconcDiff = 0.1 st := &hashSyncTester{ - tb: tb, - src: make([]rangesync.KeyBytes, cfg.numTestHashes), - opts: []rangesync.RangeSetReconcilerOption{ - rangesync.WithMaxSendRange(cfg.maxSendRange), - rangesync.WithMaxDiff(0.1), - }, + tb: tb, + src: make([]rangesync.KeyBytes, cfg.numTestHashes), + cfg: rCfg, numSpecificA: rand.Intn(cfg.maxNumSpecificA+1-cfg.minNumSpecificA) + cfg.minNumSpecificA, numSpecificB: rand.Intn(cfg.maxNumSpecificB+1-cfg.minNumSpecificB) + cfg.minNumSpecificB, } @@ -461,8 +459,9 @@ func TestSyncHash(t *testing.T) { minNumSpecificB: 4, maxNumSpecificB: 90, }) - syncA := rangesync.NewRangeSetReconciler(st.setA, st.opts...) - syncB := rangesync.NewRangeSetReconciler(st.setB, st.opts...) + logger := zap.NewNop() + syncA := rangesync.NewRangeSetReconciler(logger, st.cfg, st.setA) + syncB := rangesync.NewRangeSetReconciler(logger, st.cfg, st.setB) nRounds, nMsg, nItems := runSync(t, syncA, syncB, nil, nil, 100) numSpecific := st.numSpecificA + st.numSpecificB itemCoef := float64(nItems) / float64(numSpecific) diff --git a/sync2/rangesync/wire_conduit.go b/sync2/rangesync/wire_conduit.go index ffde72aed3..197d2f35bf 100644 --- a/sync2/rangesync/wire_conduit.go +++ b/sync2/rangesync/wire_conduit.go @@ -34,52 +34,30 @@ const ( var ErrLimitExceeded = errors.New("sync traffic/message limit exceeded") -// ConduitOption specifies an option for a message conduit. -type ConduitOption func(c *wireConduit) - -// WithTrafficLimit sets a limit on the total number of bytes sent and received. -// Zero or negative values disable the limit. -func WithTrafficLimit(limit int) ConduitOption { - return func(c *wireConduit) { - c.trafficLimit = limit - } -} - -// WithMessageLimit sets a limit on the total number of messages sent and received. -// Zero or negative values disable the limit. -func WithMessageLimit(limit int) ConduitOption { - return func(c *wireConduit) { - c.messageLimit = limit - } -} - // wireConduit is an implementation of the Conduit interface that sends and receives // messages over a stream represented by an io.ReadWriter. type wireConduit struct { - stream io.ReadWriter - eg errgroup.Group - sendCh chan SyncMessage - stopCh chan struct{} - nBytesSent atomic.Int64 - nBytesRecv atomic.Int64 - nMsgsSent atomic.Int64 - nMsgsRecv atomic.Int64 - trafficLimit int - messageLimit int + stream io.ReadWriter + cfg RangeSetReconcilerConfig + eg errgroup.Group + sendCh chan SyncMessage + stopCh chan struct{} + nBytesSent atomic.Int64 + nBytesRecv atomic.Int64 + nMsgsSent atomic.Int64 + nMsgsRecv atomic.Int64 } var _ Conduit = &wireConduit{} // startWireConduit sets up a new wireConduit using the given context, stream and options. -func startWireConduit(ctx context.Context, s io.ReadWriter, opts ...ConduitOption) *wireConduit { +func startWireConduit(ctx context.Context, s io.ReadWriter, cfg RangeSetReconcilerConfig) *wireConduit { c := &wireConduit{ stream: s, + cfg: cfg, sendCh: make(chan SyncMessage, sendQueueSize), stopCh: make(chan struct{}), } - for _, opt := range opts { - opt(c) - } c.eg.Go(func() error { defer close(c.stopCh) for { @@ -138,10 +116,10 @@ func (c *wireConduit) End() { // checkLimits checks if the traffic or message limits have been exceeded. func (c *wireConduit) checkLimits() error { - if c.trafficLimit > 0 && c.bytesSent()+c.bytesReceived() > c.trafficLimit { + if c.cfg.TrafficLimit > 0 && c.bytesSent()+c.bytesReceived() > c.cfg.TrafficLimit { return ErrLimitExceeded } - if c.messageLimit > 0 && c.messagesSent()+c.messagesReceived() > c.trafficLimit { + if c.cfg.MessageLimit > 0 && c.messagesSent()+c.messagesReceived() > c.cfg.TrafficLimit { return ErrLimitExceeded } return nil diff --git a/sync2/rangesync/wire_conduit_test.go b/sync2/rangesync/wire_conduit_test.go index b2561b3a44..435d84cfcd 100644 --- a/sync2/rangesync/wire_conduit_test.go +++ b/sync2/rangesync/wire_conduit_test.go @@ -140,7 +140,7 @@ func TestWireConduit(t *testing.T) { t, "srv", func(ctx context.Context, initialRequest []byte, stream io.ReadWriter) error { require.Equal(t, []byte("hello"), initialRequest) - c := rangesync.StartWireConduit(ctx, stream) + c := rangesync.StartWireConduit(ctx, stream, rangesync.DefaultConfig()) defer c.Stop() s := rangesync.Sender{c} require.Equal(t, []rangesync.SyncMessage{ @@ -174,7 +174,7 @@ func TestWireConduit(t *testing.T) { client := newFakeRequester(t, "client", nil, srv) require.NoError(t, client.StreamRequest(context.Background(), "srv", []byte("hello"), func(ctx context.Context, stream io.ReadWriter) error { - c := rangesync.StartWireConduit(ctx, stream) + c := rangesync.StartWireConduit(ctx, stream, rangesync.DefaultConfig()) defer c.Stop() s := rangesync.Sender{c} require.NoError(t, s.SendFingerprint(hs[0], hs[1], fp, 4)) @@ -209,27 +209,26 @@ func TestWireConduit(t *testing.T) { func TestWireConduit_Limits(t *testing.T) { for _, tc := range []struct { - name string - opts []rangesync.ConduitOption - error bool + name string + trafficLimit int + messageLimit int + error bool }{ { - name: "message limit hit", - opts: []rangesync.ConduitOption{rangesync.WithMessageLimit(10)}, - error: true, + name: "message limit hit", + messageLimit: 10, + error: true, }, { - name: "traffic limit hit", - opts: []rangesync.ConduitOption{rangesync.WithTrafficLimit(100)}, - error: true, + name: "traffic limit hit", + trafficLimit: 100, + error: true, }, { - name: "limits not hit", - opts: []rangesync.ConduitOption{ - rangesync.WithMessageLimit(1000), - rangesync.WithTrafficLimit(10000), - }, - error: false, + name: "limits not hit", + trafficLimit: 10000, + messageLimit: 1000, + error: false, }, } { t.Run(tc.name, func(t *testing.T) { @@ -237,7 +236,10 @@ func TestWireConduit_Limits(t *testing.T) { srv := newFakeRequester( t, "srv", func(ctx context.Context, initialRequest []byte, stream io.ReadWriter) error { - c := rangesync.StartWireConduit(ctx, stream, tc.opts...) + cfg := rangesync.DefaultConfig() + cfg.TrafficLimit = tc.trafficLimit + cfg.MessageLimit = tc.messageLimit + c := rangesync.StartWireConduit(ctx, stream, cfg) defer c.Stop() for range 11 { msg, err := c.NextMessage() @@ -266,7 +268,8 @@ func TestWireConduit_Limits(t *testing.T) { eg.Go(func() error { client.StreamRequest(ctx, "srv", []byte("hello"), func(ctx context.Context, stream io.ReadWriter) error { - c := rangesync.StartWireConduit(ctx, stream) + c := rangesync.StartWireConduit( + ctx, stream, rangesync.DefaultConfig()) defer c.Stop() s := rangesync.Sender{c} for i := 0; i < 11; i++ { @@ -308,7 +311,7 @@ func TestWireConduit_StopSend(t *testing.T) { defer cancel() client.StreamRequest(ctx, "srv", []byte("hello"), func(ctx context.Context, stream io.ReadWriter) error { - c := rangesync.StartWireConduit(ctx, stream) + c := rangesync.StartWireConduit(ctx, stream, rangesync.DefaultConfig()) s := rangesync.Sender{c} // The actual message is enqueued but not sent s.SendDone() From 287d76da63977e9d60532d1bb93620b6ebb3c018 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Thu, 31 Oct 2024 03:15:20 +0400 Subject: [PATCH 08/17] sync2: multipeer: add error decoration --- sync2/multipeer/setsyncbase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sync2/multipeer/setsyncbase.go b/sync2/multipeer/setsyncbase.go index 3aadd6344a..d3b5bface2 100644 --- a/sync2/multipeer/setsyncbase.go +++ b/sync2/multipeer/setsyncbase.go @@ -99,7 +99,7 @@ func (ssb *SetSyncBase) receiveKey(k rangesync.KeyBytes, p p2p.Peer) error { key := k.String() has, err := ssb.os.Has(k) if err != nil { - return err + return fmt.Errorf("checking if the key is present: %w", err) } if !has { ssb.waiting = append(ssb.waiting, From 51166fa921f61c1c0066a4e5cfe854cd03efca87 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Thu, 31 Oct 2024 03:21:14 +0400 Subject: [PATCH 09/17] sync2: remove Dispatcher type alias --- sync2/p2p.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sync2/p2p.go b/sync2/p2p.go index 6886055593..8fbd200478 100644 --- a/sync2/p2p.go +++ b/sync2/p2p.go @@ -18,8 +18,6 @@ import ( "github.com/spacemeshos/go-spacemesh/sync2/rangesync" ) -type Dispatcher = rangesync.Dispatcher - // Config contains the configuration for the P2PHashSync. type Config struct { rangesync.RangeSetReconcilerConfig `mapstructure:",squash"` @@ -55,7 +53,7 @@ type P2PHashSync struct { // NewP2PHashSync creates a new P2PHashSync. func NewP2PHashSync( logger *zap.Logger, - d *Dispatcher, + d *rangesync.Dispatcher, name string, os rangesync.OrderedSet, keyLen, maxDepth int, From c58d690ca5eb6ce71caca0d2ce66d2d8aa4e4bf4 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Thu, 31 Oct 2024 06:23:26 +0400 Subject: [PATCH 10/17] sync2: multipeer: refactor tests --- sync2/multipeer/multipeer_test.go | 124 ++++++++++++++++-------------- 1 file changed, 68 insertions(+), 56 deletions(-) diff --git a/sync2/multipeer/multipeer_test.go b/sync2/multipeer/multipeer_test.go index 25a1cd3d54..ab0a516839 100644 --- a/sync2/multipeer/multipeer_test.go +++ b/sync2/multipeer/multipeer_test.go @@ -21,6 +21,11 @@ import ( "github.com/spacemeshos/go-spacemesh/sync2/rangesync" ) +const ( + numSyncs = 3 + numSyncPeers = 6 +) + // FIXME: BlockUntilContext is not included in FakeClock interface. // This will be fixed in a post-0.4.0 clockwork release, but with a breaking change that // makes FakeClock a struct instead of an interface. @@ -78,7 +83,7 @@ func newMultiPeerSyncTester(t *testing.T, addPeers int) *multiPeerSyncTester { } cfg := multipeer.DefaultConfig() cfg.SyncInterval = time.Minute - cfg.SyncPeerCount = 6 + cfg.SyncPeerCount = numSyncPeers cfg.MinSplitSyncPeers = 2 cfg.MinSplitSyncCount = 90 cfg.MaxFullDiff = 20 @@ -170,8 +175,6 @@ func (mt *multiPeerSyncTester) satisfy() { } func TestMultiPeerSync(t *testing.T) { - const numSyncs = 3 - t.Run("split sync", func(t *testing.T) { mt := newMultiPeerSyncTester(t, 0) ctx := mt.start() @@ -185,7 +188,7 @@ func TestMultiPeerSync(t *testing.T) { // randomly and probed mt.syncBase.EXPECT().Count().Return(50, nil).AnyTimes() for i := 0; i < numSyncs; i++ { - plSplit := mt.expectProbe(6, rangesync.ProbeResult{ + plSplit := mt.expectProbe(numSyncPeers, rangesync.ProbeResult{ FP: "foo", Count: 100, Sim: 0.5, // too low for full sync @@ -197,12 +200,12 @@ func TestMultiPeerSync(t *testing.T) { }) mt.syncBase.EXPECT().Wait() mt.clock.BlockUntilContext(ctx, 1) - plFull := mt.expectProbe(6, rangesync.ProbeResult{ + plFull := mt.expectProbe(numSyncPeers, rangesync.ProbeResult{ FP: "foo", Count: 100, Sim: 1, // after sync }) - mt.expectFullSync(plFull, 6, 0) + mt.expectFullSync(plFull, numSyncPeers, 0) mt.syncBase.EXPECT().Wait() if i > 0 { mt.clock.Advance(time.Minute) @@ -218,22 +221,23 @@ func TestMultiPeerSync(t *testing.T) { mt := newMultiPeerSyncTester(t, 10) mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() require.False(t, mt.reconciler.Synced()) - var ctx context.Context - for i := 0; i < numSyncs; i++ { - pl := mt.expectProbe(6, rangesync.ProbeResult{ + expect := func() { + pl := mt.expectProbe(numSyncPeers, rangesync.ProbeResult{ FP: "foo", Count: 100, Sim: 0.99, // high enough for full sync }) - mt.expectFullSync(pl, 6, 0) + mt.expectFullSync(pl, numSyncPeers, 0) mt.syncBase.EXPECT().Wait() - if i == 0 { - //nolint:fatcontext - ctx = mt.start() - } else { - // first full sync happens immediately - mt.clock.Advance(time.Minute) - } + } + expect() + // first full sync happens immediately + ctx := mt.start() + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + for i := 0; i < numSyncs; i++ { + expect() + mt.clock.Advance(time.Minute) mt.clock.BlockUntilContext(ctx, 1) mt.satisfy() } @@ -243,11 +247,10 @@ func TestMultiPeerSync(t *testing.T) { t.Run("full sync, peers with low count ignored", func(t *testing.T) { mt := newMultiPeerSyncTester(t, 0) - addedPeers := mt.addPeers(6) + addedPeers := mt.addPeers(numSyncPeers) mt.syncBase.EXPECT().Count().Return(1000, nil).AnyTimes() require.False(t, mt.reconciler.Synced()) - var ctx context.Context - for i := 0; i < numSyncs; i++ { + expect := func() { var pl peerList for _, p := range addedPeers[:5] { mt.expectSingleProbe(p, rangesync.ProbeResult{ @@ -264,13 +267,15 @@ func TestMultiPeerSync(t *testing.T) { }) mt.expectFullSync(&pl, 5, 0) mt.syncBase.EXPECT().Wait() - if i == 0 { - //nolint:fatcontext - ctx = mt.start() - } else { - // first full sync happens immediately - mt.clock.Advance(time.Minute) - } + } + expect() + // first full sync happens immediately + ctx := mt.start() + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + for i := 1; i < numSyncs; i++ { + expect() + mt.clock.Advance(time.Minute) mt.clock.BlockUntilContext(ctx, 1) mt.satisfy() } @@ -281,8 +286,7 @@ func TestMultiPeerSync(t *testing.T) { t.Run("full sync due to low peer count", func(t *testing.T) { mt := newMultiPeerSyncTester(t, 1) mt.syncBase.EXPECT().Count().Return(50, nil).AnyTimes() - var ctx context.Context - for i := 0; i < numSyncs; i++ { + expect := func() { pl := mt.expectProbe(1, rangesync.ProbeResult{ FP: "foo", Count: 100, @@ -290,15 +294,18 @@ func TestMultiPeerSync(t *testing.T) { }) mt.expectFullSync(pl, 1, 0) mt.syncBase.EXPECT().Wait() - if i == 0 { - //nolint:fatcontext - ctx = mt.start() - } else { - mt.clock.Advance(time.Minute) - } + } + expect() + ctx := mt.start() + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + for i := 1; i < numSyncs; i++ { + expect() + mt.clock.Advance(time.Minute) mt.clock.BlockUntilContext(ctx, 1) mt.satisfy() } + require.True(t, mt.reconciler.Synced()) mt.syncBase.EXPECT().Wait() }) @@ -316,49 +323,54 @@ func TestMultiPeerSync(t *testing.T) { }) t.Run("failed peers during full sync", func(t *testing.T) { + const numFails = 3 mt := newMultiPeerSyncTester(t, 10) mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() - var ctx context.Context - for i := 0; i < numSyncs; i++ { - pl := mt.expectProbe(6, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) - mt.expectFullSync(pl, 6, 3) + expect := func() { + pl := mt.expectProbe(numSyncPeers, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) + mt.expectFullSync(pl, numSyncPeers, numFails) mt.syncBase.EXPECT().Wait() - if i == 0 { - //nolint:fatcontext - ctx = mt.start() - } else { - mt.clock.Advance(time.Minute) - } + } + expect() + ctx := mt.start() + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + for i := 1; i < numSyncs; i++ { + expect() + mt.clock.Advance(time.Minute) mt.clock.BlockUntilContext(ctx, 1) mt.satisfy() } + require.True(t, mt.reconciler.Synced()) mt.syncBase.EXPECT().Wait() }) t.Run("failed synced key handling during full sync", func(t *testing.T) { mt := newMultiPeerSyncTester(t, 10) mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() - var ctx context.Context - for i := 0; i < numSyncs; i++ { - pl := mt.expectProbe(6, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) - mt.expectFullSync(pl, 6, 0) + expect := func() { + pl := mt.expectProbe(numSyncPeers, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) + mt.expectFullSync(pl, numSyncPeers, 0) mt.syncBase.EXPECT().Wait().Return(errors.New("some handlers failed")) - if i == 0 { - //nolint:fatcontext - ctx = mt.start() - } else { - mt.clock.Advance(time.Minute) - } + } + expect() + ctx := mt.start() + mt.clock.BlockUntilContext(ctx, 1) + mt.satisfy() + for i := 0; i < numSyncs; i++ { + expect() + mt.clock.Advance(time.Minute) mt.clock.BlockUntilContext(ctx, 1) mt.satisfy() } + require.True(t, mt.reconciler.Synced()) mt.syncBase.EXPECT().Wait() }) t.Run("cancellation during sync", func(t *testing.T) { mt := newMultiPeerSyncTester(t, 10) mt.syncBase.EXPECT().Count().Return(100, nil).AnyTimes() - mt.expectProbe(6, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) + mt.expectProbe(numSyncPeers, rangesync.ProbeResult{FP: "foo", Count: 100, Sim: 0.99}) mt.syncRunner.EXPECT().FullSync(gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, peers []p2p.Peer) error { mt.cancel() From cf46587b3adc965c15559f80f52e9e4d69cee2ee Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Thu, 31 Oct 2024 06:42:25 +0400 Subject: [PATCH 11/17] p2p: server: pass PeerID as an explicit argument to the handler --- fetch/fetch.go | 4 ++-- fetch/fetch_test.go | 2 +- fetch/handler.go | 21 +++++++++--------- fetch/handler_test.go | 25 ++++++++++++---------- fetch/mesh_data_test.go | 16 +++++++------- hare4/hare.go | 2 +- hare4/hare_test.go | 4 ++-- p2p/server/server.go | 32 +++++++--------------------- p2p/server/server_test.go | 13 ++++------- sync2/p2p.go | 8 ++----- sync2/rangesync/dispatcher.go | 6 ++++-- sync2/rangesync/dispatcher_test.go | 3 ++- sync2/rangesync/p2p.go | 2 +- sync2/rangesync/wire_conduit_test.go | 13 +++++++---- 14 files changed, 69 insertions(+), 82 deletions(-) diff --git a/fetch/fetch.go b/fetch/fetch.go index 30447be10a..396342a4bb 100644 --- a/fetch/fetch.go +++ b/fetch/fetch.go @@ -329,7 +329,7 @@ func NewFetch( f.registerServer(host, hashProtocol, h.handleHashReqStream) f.registerServer( host, activeSetProtocol, - func(ctx context.Context, msg []byte, s io.ReadWriter) error { + func(ctx context.Context, _ p2p.Peer, msg []byte, s io.ReadWriter) error { return h.doHandleHashReqStream(ctx, msg, s, datastore.ActiveSet) }) f.registerServer(host, meshHashProtocol, h.handleMeshHashReqStream) @@ -339,7 +339,7 @@ func NewFetch( f.registerServer(host, hashProtocol, server.WrapHandler(h.handleHashReq)) f.registerServer( host, activeSetProtocol, - server.WrapHandler(func(ctx context.Context, data []byte) ([]byte, error) { + server.WrapHandler(func(ctx context.Context, _ p2p.Peer, data []byte) ([]byte, error) { return h.doHandleHashReq(ctx, data, datastore.ActiveSet) })) f.registerServer(host, meshHashProtocol, server.WrapHandler(h.handleMeshHashReq)) diff --git a/fetch/fetch_test.go b/fetch/fetch_test.go index 4d32ef96a5..50e81d9fe7 100644 --- a/fetch/fetch_test.go +++ b/fetch/fetch_test.go @@ -374,7 +374,7 @@ func TestFetch_PeerDroppedWhenMessageResultsInValidationReject(t *testing.T) { require.Len(t, h.GetPeers(), 1) // This handler returns a ResponseBatch with an empty response that will fail validation on the remote peer - badPeerHandler := func(_ context.Context, data []byte) ([]byte, error) { + badPeerHandler := func(_ context.Context, _ p2p.Peer, data []byte) ([]byte, error) { var b RequestBatch codec.Decode(data, &b) diff --git a/fetch/handler.go b/fetch/handler.go index 371377cc50..f04488019a 100644 --- a/fetch/handler.go +++ b/fetch/handler.go @@ -13,6 +13,7 @@ import ( "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/log" + "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/server" "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sql/atxs" @@ -45,7 +46,7 @@ func newHandler( } // handleMaliciousIDsReq returns the IDs of all known malicious nodes. -func (h *handler) handleMaliciousIDsReq(ctx context.Context, _ []byte) ([]byte, error) { +func (h *handler) handleMaliciousIDsReq(ctx context.Context, _ p2p.Peer, _ []byte) ([]byte, error) { nodes, err := identities.AllMalicious(h.cdb) if err != nil { return nil, fmt.Errorf("getting malicious IDs: %w", err) @@ -57,7 +58,7 @@ func (h *handler) handleMaliciousIDsReq(ctx context.Context, _ []byte) ([]byte, return codec.MustEncode(malicious), nil } -func (h *handler) handleMaliciousIDsReqStream(ctx context.Context, msg []byte, s io.ReadWriter) error { +func (h *handler) handleMaliciousIDsReqStream(ctx context.Context, _ p2p.Peer, msg []byte, s io.ReadWriter) error { if err := h.streamIDs(ctx, s, func(cbk retrieveCallback) error { nodeIDs, err := identities.AllMalicious(h.cdb) if err != nil { @@ -75,7 +76,7 @@ func (h *handler) handleMaliciousIDsReqStream(ctx context.Context, msg []byte, s } // handleEpochInfoReq returns the ATXs published in the specified epoch. -func (h *handler) handleEpochInfoReq(ctx context.Context, msg []byte) ([]byte, error) { +func (h *handler) handleEpochInfoReq(ctx context.Context, _ p2p.Peer, msg []byte) ([]byte, error) { var epoch types.EpochID if err := codec.Decode(msg, &epoch); err != nil { return nil, err @@ -103,7 +104,7 @@ func (h *handler) handleEpochInfoReq(ctx context.Context, msg []byte) ([]byte, e } // handleEpochInfoReq streams the ATXs published in the specified epoch. -func (h *handler) handleEpochInfoReqStream(ctx context.Context, msg []byte, s io.ReadWriter) error { +func (h *handler) handleEpochInfoReqStream(ctx context.Context, _ p2p.Peer, msg []byte, s io.ReadWriter) error { var epoch types.EpochID if err := codec.Decode(msg, &epoch); err != nil { return err @@ -181,7 +182,7 @@ func (h *handler) streamIDs(ctx context.Context, s io.ReadWriter, retrieve retri } // handleLayerDataReq returns all data in a layer, described in LayerData. -func (h *handler) handleLayerDataReq(ctx context.Context, req []byte) ([]byte, error) { +func (h *handler) handleLayerDataReq(ctx context.Context, _ p2p.Peer, req []byte) ([]byte, error) { var ( lid types.LayerID ld LayerData @@ -202,7 +203,7 @@ func (h *handler) handleLayerDataReq(ctx context.Context, req []byte) ([]byte, e return out, nil } -func (h *handler) handleLayerOpinionsReq2(ctx context.Context, data []byte) ([]byte, error) { +func (h *handler) handleLayerOpinionsReq2(ctx context.Context, _ p2p.Peer, data []byte) ([]byte, error) { var req OpinionRequest if err := codec.Decode(data, &req); err != nil { return nil, err @@ -257,7 +258,7 @@ func (h *handler) handleCertReq(ctx context.Context, lid types.LayerID, bid type return nil, err } -func (h *handler) handleHashReq(ctx context.Context, data []byte) ([]byte, error) { +func (h *handler) handleHashReq(ctx context.Context, _ p2p.Peer, data []byte) ([]byte, error) { return h.doHandleHashReq(ctx, data, datastore.NoHint) } @@ -327,7 +328,7 @@ func (h *handler) doHandleHashReq(ctx context.Context, data []byte, hint datasto return bts, nil } -func (h *handler) handleHashReqStream(ctx context.Context, msg []byte, s io.ReadWriter) error { +func (h *handler) handleHashReqStream(ctx context.Context, _ p2p.Peer, msg []byte, s io.ReadWriter) error { return h.doHandleHashReqStream(ctx, msg, s, datastore.NoHint) } @@ -416,7 +417,7 @@ func (h *handler) doHandleHashReqStream( return nil } -func (h *handler) handleMeshHashReq(ctx context.Context, reqData []byte) ([]byte, error) { +func (h *handler) handleMeshHashReq(ctx context.Context, _ p2p.Peer, reqData []byte) ([]byte, error) { var ( req MeshHashRequest hashes []types.Hash32 @@ -447,7 +448,7 @@ func (h *handler) handleMeshHashReq(ctx context.Context, reqData []byte) ([]byte return data, nil } -func (h *handler) handleMeshHashReqStream(ctx context.Context, reqData []byte, s io.ReadWriter) error { +func (h *handler) handleMeshHashReqStream(ctx context.Context, _ p2p.Peer, reqData []byte, s io.ReadWriter) error { var req MeshHashRequest if err := codec.Decode(reqData, &req); err != nil { return fmt.Errorf("%w: decoding request: %w", errBadRequest, err) diff --git a/fetch/handler_test.go b/fetch/handler_test.go index 0989761b2e..b3176268a0 100644 --- a/fetch/handler_test.go +++ b/fetch/handler_test.go @@ -12,6 +12,7 @@ import ( "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" + "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/server" "github.com/spacemeshos/go-spacemesh/proposals/store" "github.com/spacemeshos/go-spacemesh/signing" @@ -108,7 +109,7 @@ func TestHandleLayerDataReq(t *testing.T) { lidBytes, err := codec.Encode(&lid) require.NoError(t, err) - out, err := th.handleLayerDataReq(context.Background(), lidBytes) + out, err := th.handleLayerDataReq(context.Background(), p2p.Peer(""), lidBytes) require.NoError(t, err) var got LayerData err = codec.Decode(out, &got) @@ -155,7 +156,7 @@ func TestHandleLayerOpinionsReq(t *testing.T) { reqBytes, err := codec.Encode(&req) require.NoError(t, err) - out, err := th.handleLayerOpinionsReq2(context.Background(), reqBytes) + out, err := th.handleLayerOpinionsReq2(context.Background(), p2p.Peer(""), reqBytes) require.NoError(t, err) var got LayerOpinion @@ -182,14 +183,14 @@ func TestHandleCertReq(t *testing.T) { reqData, err := codec.Encode(req) require.NoError(t, err) - resp, err := th.handleLayerOpinionsReq2(context.Background(), reqData) + resp, err := th.handleLayerOpinionsReq2(context.Background(), p2p.Peer(""), reqData) require.ErrorIs(t, err, sql.ErrNotFound) require.Nil(t, resp) cert := &types.Certificate{BlockID: bid} require.NoError(t, certificates.Add(th.cdb, lid, cert)) - resp, err = th.handleLayerOpinionsReq2(context.Background(), reqData) + resp, err = th.handleLayerOpinionsReq2(context.Background(), p2p.Peer(""), reqData) require.NoError(t, err) require.NotNil(t, resp) var got types.Certificate @@ -249,7 +250,7 @@ func TestHandleMeshHashReq(t *testing.T) { reqData, err := codec.Encode(req) require.NoError(t, err) - resp, err := th.handleMeshHashReq(context.Background(), reqData) + resp, err := th.handleMeshHashReq(context.Background(), p2p.Peer(""), reqData) if tc.err == nil { require.NoError(t, err) got, err := codec.DecodeSlice[types.Hash32](resp) @@ -308,7 +309,7 @@ func TestHandleEpochInfoReq(t *testing.T) { require.NoError(t, err) t.Run("non-streamed", func(t *testing.T) { - out, err := th.handleEpochInfoReq(context.Background(), epochBytes) + out, err := th.handleEpochInfoReq(context.Background(), p2p.Peer(""), epochBytes) require.NoError(t, err) var got EpochData require.NoError(t, codec.Decode(out, &got)) @@ -317,7 +318,8 @@ func TestHandleEpochInfoReq(t *testing.T) { t.Run("streamed", func(t *testing.T) { var b bytes.Buffer - require.NoError(t, th.handleEpochInfoReqStream(context.Background(), epochBytes, &b)) + require.NoError(t, th.handleEpochInfoReqStream(context.Background(), + p2p.Peer(""), epochBytes, &b)) var resp server.Response require.NoError(t, codec.Decode(b.Bytes(), &resp)) var got EpochData @@ -328,7 +330,8 @@ func TestHandleEpochInfoReq(t *testing.T) { t.Run("streamed request failure", func(t *testing.T) { th.db.Close() var b bytes.Buffer - require.NoError(t, th.handleEpochInfoReqStream(context.Background(), epochBytes, &b)) + require.NoError(t, th.handleEpochInfoReqStream(context.Background(), + p2p.Peer(""), epochBytes, &b)) var resp server.Response require.NoError(t, codec.Decode(b.Bytes(), &resp)) require.Empty(t, resp.Data) @@ -383,7 +386,7 @@ func testHandleEpochInfoReqWithQueryCache( func TestHandleEpochInfoReqWithQueryCache(t *testing.T) { testHandleEpochInfoReqWithQueryCache(t, func(th *testHandler, req []byte, ed *EpochData) { - out, err := th.handleEpochInfoReq(context.Background(), req) + out, err := th.handleEpochInfoReq(context.Background(), p2p.Peer(""), req) require.NoError(t, err) require.NoError(t, codec.Decode(out, ed)) }) @@ -392,7 +395,7 @@ func TestHandleEpochInfoReqWithQueryCache(t *testing.T) { func TestHandleEpochInfoReqStreamWithQueryCache(t *testing.T) { testHandleEpochInfoReqWithQueryCache(t, func(th *testHandler, req []byte, ed *EpochData) { var b bytes.Buffer - err := th.handleEpochInfoReqStream(context.Background(), req, &b) + err := th.handleEpochInfoReqStream(context.Background(), p2p.Peer(""), req, &b) require.NoError(t, err) n, err := server.ReadResponse(&b, func(resLen uint32) (int, error) { return codec.DecodeFrom(&b, ed) @@ -428,7 +431,7 @@ func TestHandleMaliciousIDsReq(t *testing.T) { require.NoError(t, identities.SetMalicious(th.cdb, nid, types.RandomBytes(11), time.Now())) } - out, err := th.handleMaliciousIDsReq(context.TODO(), []byte{}) + out, err := th.handleMaliciousIDsReq(context.TODO(), p2p.Peer(""), []byte{}) require.NoError(t, err) var got MaliciousIDs require.NoError(t, codec.Decode(out, &got)) diff --git a/fetch/mesh_data_test.go b/fetch/mesh_data_test.go index 0fb43b7831..713c96a7b7 100644 --- a/fetch/mesh_data_test.go +++ b/fetch/mesh_data_test.go @@ -960,7 +960,7 @@ func Test_GetAtxsLimiting(t *testing.T) { srv := server.New( wrapHost(mesh.Hosts()[1]), hashProtocol, - server.WrapHandler(func(_ context.Context, data []byte) ([]byte, error) { + server.WrapHandler(func(_ context.Context, _ p2p.Peer, data []byte) ([]byte, error) { var requestBatch RequestBatch require.NoError(t, codec.Decode(data, &requestBatch)) resBatch := ResponseBatch{ @@ -1120,14 +1120,14 @@ func TestBatchErrorIgnore(t *testing.T) { func FuzzCertRequest(f *testing.F) { h := createTestHandler(f) f.Fuzz(func(t *testing.T, data []byte) { - h.handleLayerOpinionsReq2(context.Background(), data) + h.handleLayerOpinionsReq2(context.Background(), p2p.Peer(""), data) }) } func FuzzMeshHashRequest(f *testing.F) { h := createTestHandler(f) f.Fuzz(func(t *testing.T, data []byte) { - h.handleMeshHashReq(context.Background(), data) + h.handleMeshHashReq(context.Background(), p2p.Peer(""), data) }) } @@ -1135,14 +1135,14 @@ func FuzzMeshHashRequestStream(f *testing.F) { h := createTestHandler(f) f.Fuzz(func(t *testing.T, data []byte) { var b bytes.Buffer - h.handleMeshHashReqStream(context.Background(), data, &b) + h.handleMeshHashReqStream(context.Background(), p2p.Peer(""), data, &b) }) } func FuzzLayerInfo(f *testing.F) { h := createTestHandler(f) f.Fuzz(func(t *testing.T, data []byte) { - h.handleEpochInfoReq(context.Background(), data) + h.handleEpochInfoReq(context.Background(), p2p.Peer(""), data) }) } @@ -1150,14 +1150,14 @@ func FuzzLayerInfoStream(f *testing.F) { h := createTestHandler(f) f.Fuzz(func(t *testing.T, data []byte) { var b bytes.Buffer - h.handleEpochInfoReqStream(context.Background(), data, &b) + h.handleEpochInfoReqStream(context.Background(), p2p.Peer(""), data, &b) }) } func FuzzHashReq(f *testing.F) { h := createTestHandler(f) f.Fuzz(func(t *testing.T, data []byte) { - h.handleHashReq(context.Background(), data) + h.handleHashReq(context.Background(), p2p.Peer(""), data) }) } @@ -1165,6 +1165,6 @@ func FuzzHashReqStream(f *testing.F) { h := createTestHandler(f) f.Fuzz(func(t *testing.T, data []byte) { var b bytes.Buffer - h.handleHashReqStream(context.Background(), data, &b) + h.handleHashReqStream(context.Background(), p2p.Peer(""), data, &b) }) } diff --git a/hare4/hare.go b/hare4/hare.go index 5242658406..b3f48dba17 100644 --- a/hare4/hare.go +++ b/hare4/hare.go @@ -361,7 +361,7 @@ func (h *Hare) fetchFull(ctx context.Context, peer p2p.Peer, msgId types.Hash32) return resp.Ids, nil } -func (h *Hare) handleProposalsStream(ctx context.Context, msg []byte, s io.ReadWriter) error { +func (h *Hare) handleProposalsStream(ctx context.Context, _ p2p.Peer, msg []byte, s io.ReadWriter) error { requestCompactHandlerCounter.Inc() compactProps := &CompactIdRequest{} if err := codec.Decode(msg, compactProps); err != nil { diff --git a/hare4/hare_test.go b/hare4/hare_test.go index 04fd0ce4c9..bb30d023bf 100644 --- a/hare4/hare_test.go +++ b/hare4/hare_test.go @@ -540,7 +540,7 @@ func (cl *lockstepCluster) setup() { if other.peerId() == p { b := make([]byte, 0, 1024) buf := bytes.NewBuffer(b) - other.hare.handleProposalsStream(ctx, msg, buf) + other.hare.handleProposalsStream(ctx, p, msg, buf) cb(ctx, buf) } } @@ -1211,7 +1211,7 @@ func TestHare_ReconstructForward(t *testing.T) { if other.peerId() == p { b := make([]byte, 0, 1024) buf := bytes.NewBuffer(b) - if err := other.hare.handleProposalsStream(ctx, msg, buf); err != nil { + if err := other.hare.handleProposalsStream(ctx, p, msg, buf); err != nil { return fmt.Errorf("exec handleProposalStream: %w", err) } if err := cb(ctx, buf); err != nil { diff --git a/p2p/server/server.go b/p2p/server/server.go index d9ae545b9c..88a32f2c54 100644 --- a/p2p/server/server.go +++ b/p2p/server/server.go @@ -112,28 +112,12 @@ func WithDecayingTag(tag DecayingTagSpec) Opt { } } -type peerIDKey struct{} - -func withPeerID(ctx context.Context, peerID peer.ID) context.Context { - return context.WithValue(ctx, peerIDKey{}, peerID) -} - -// ContextPeerID retrieves the ID of the peer being served from the context and a boolean -// value indicating that the context contains peer ID. If there's no peer ID associated -// with the context, the function returns an empty peer ID and false. -func ContextPeerID(ctx context.Context) (peer.ID, bool) { - if v := ctx.Value(peerIDKey{}); v != nil { - return v.(peer.ID), true - } - return peer.ID(""), false -} - // Handler is a handler to be defined by the application. -type Handler func(context.Context, []byte) ([]byte, error) +type Handler func(context.Context, peer.ID, []byte) ([]byte, error) // StreamHandler is a handler that writes the response to the stream directly instead of // buffering the serialized representation. -type StreamHandler func(context.Context, []byte, io.ReadWriter) error +type StreamHandler func(context.Context, peer.ID, []byte, io.ReadWriter) error // StreamRequestCallback is a function that executes a streamed request. type StreamRequestCallback func(context.Context, io.ReadWriter) error @@ -283,7 +267,7 @@ func (s *Server) Run(ctx context.Context) error { return nil } peer := req.stream.Conn().RemotePeer() - ctx, cancel := context.WithCancel(withPeerID(ctx, peer)) + ctx, cancel := context.WithCancel(ctx) eg.Go(func() error { <-ctx.Done() s.sem.Release(1) @@ -296,7 +280,7 @@ func (s *Server) Run(ctx context.Context) error { if s.decayingTag != nil { s.decayingTag.Bump(peer, s.decayingTagSpec.Inc) } - ok := s.queueHandler(ctx, req.stream) + ok := s.queueHandler(ctx, peer, req.stream) duration := time.Since(req.received) if s.peerInfo() != nil { info := s.peerInfo().EnsurePeerInfo(conn.RemotePeer()) @@ -316,7 +300,7 @@ func (s *Server) Run(ctx context.Context) error { } } -func (s *Server) queueHandler(ctx context.Context, stream network.Stream) bool { +func (s *Server) queueHandler(ctx context.Context, peer peer.ID, stream network.Stream) bool { dadj := newDeadlineAdjuster(stream, s.timeout, s.hardTimeout) defer dadj.Close() rd := bufio.NewReader(dadj) @@ -353,7 +337,7 @@ func (s *Server) queueHandler(ctx context.Context, stream network.Stream) bool { return false } start := time.Now() - if err = s.handler(log.WithNewRequestID(ctx), buf, dadj); err != nil { + if err = s.handler(log.WithNewRequestID(ctx), peer, buf, dadj); err != nil { s.logger.Debug("handler reported error", zap.String("protocol", s.protocol), zap.Stringer("remotePeer", stream.Conn().RemotePeer()), @@ -562,8 +546,8 @@ func ReadResponse(r io.Reader, toCall func(resLen uint32) (int, error)) (int, er } func WrapHandler(handler Handler) StreamHandler { - return func(ctx context.Context, req []byte, stream io.ReadWriter) error { - buf, hErr := handler(ctx, req) + return func(ctx context.Context, peer peer.ID, req []byte, stream io.ReadWriter) error { + buf, hErr := handler(ctx, peer, req) var resp Response if hErr != nil { resp.Error = hErr.Error() diff --git a/p2p/server/server_test.go b/p2p/server/server_test.go index 9b0553f6e0..49e27c75b0 100644 --- a/p2p/server/server_test.go +++ b/p2p/server/server_test.go @@ -46,12 +46,10 @@ func TestServer(t *testing.T) { request := []byte("test request") testErr := errors.New("test error") - handler := func(ctx context.Context, msg []byte) ([]byte, error) { - peerID, found := ContextPeerID(ctx) - require.True(t, found) + handler := func(ctx context.Context, peerID peer.ID, msg []byte) ([]byte, error) { return append(msg, []byte(peerID)...), nil } - errhandler := func(_ context.Context, _ []byte) ([]byte, error) { + errhandler := func(_ context.Context, _ peer.ID, _ []byte) ([]byte, error) { return nil, testErr } opts := []Opt{ @@ -84,9 +82,6 @@ func TestServer(t *testing.T) { append(opts, WithRequestSizeLimit(limit))..., ) ctx, cancel := context.WithCancel(context.Background()) - noPeerID, found := ContextPeerID(ctx) - require.Equal(t, peer.ID(""), noPeerID) - require.False(t, found) var eg errgroup.Group eg.Go(func() error { return srv1.Run(ctx) @@ -196,7 +191,7 @@ func Test_Queued(t *testing.T) { srv := New( wrapHost(t, mesh.Hosts()[1]), proto, - WrapHandler(func(_ context.Context, msg []byte) ([]byte, error) { + WrapHandler(func(_ context.Context, _ peer.ID, msg []byte) ([]byte, error) { wg.Done() <-stop return msg, nil @@ -248,7 +243,7 @@ func Test_RequestInterval(t *testing.T) { srv := New( wrapHost(t, mesh.Hosts()[1]), proto, - WrapHandler(func(_ context.Context, msg []byte) ([]byte, error) { + WrapHandler(func(_ context.Context, _ peer.ID, msg []byte) ([]byte, error) { return msg, nil }), WithRequestsPerInterval(maxReq, maxReqTime), diff --git a/sync2/p2p.go b/sync2/p2p.go index 8fbd200478..d36d7546fd 100644 --- a/sync2/p2p.go +++ b/sync2/p2p.go @@ -13,7 +13,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/spacemeshos/go-spacemesh/fetch/peers" - "github.com/spacemeshos/go-spacemesh/p2p/server" + "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/sync2/multipeer" "github.com/spacemeshos/go-spacemesh/sync2/rangesync" ) @@ -77,11 +77,7 @@ func NewP2PHashSync( return s } -func (s *P2PHashSync) serve(ctx context.Context, stream io.ReadWriter) error { - peer, found := server.ContextPeerID(ctx) - if !found { - panic("BUG: no peer ID found in the handler") - } +func (s *P2PHashSync) serve(ctx context.Context, peer p2p.Peer, stream io.ReadWriter) error { // We derive a dedicated Syncer for the peer being served to pass all the received // items through the handler before adding them to the main ItemStore return s.syncBase.Derive(peer).Serve(ctx, stream) diff --git a/sync2/rangesync/dispatcher.go b/sync2/rangesync/dispatcher.go index a255402831..726e1e1d27 100644 --- a/sync2/rangesync/dispatcher.go +++ b/sync2/rangesync/dispatcher.go @@ -9,11 +9,12 @@ import ( "github.com/libp2p/go-libp2p/core/host" "go.uber.org/zap" + "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/server" ) // Handler is a function that handles a request for a Dispatcher. -type Handler func(ctx context.Context, s io.ReadWriter) error +type Handler func(context.Context, p2p.Peer, io.ReadWriter) error // Dispatcher multiplexes a P2P Server to multiple set reconcilers. type Dispatcher struct { @@ -51,6 +52,7 @@ func (d *Dispatcher) Register(name string, h Handler) { // Dispatch dispatches a request to a handler. func (d *Dispatcher) Dispatch( ctx context.Context, + peer p2p.Peer, req []byte, stream io.ReadWriter, ) (err error) { @@ -62,7 +64,7 @@ func (d *Dispatcher) Dispatch( return fmt.Errorf("no handler named %q", name) } d.logger.Debug("dispatch", zap.String("handler", name)) - if err := h(ctx, stream); err != nil { + if err := h(ctx, peer, stream); err != nil { return fmt.Errorf("handler %q: %w", name, err) } return nil diff --git a/sync2/rangesync/dispatcher_test.go b/sync2/rangesync/dispatcher_test.go index 3b4cb49753..4df3e1ff63 100644 --- a/sync2/rangesync/dispatcher_test.go +++ b/sync2/rangesync/dispatcher_test.go @@ -11,12 +11,13 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" + "github.com/spacemeshos/go-spacemesh/p2p" "github.com/spacemeshos/go-spacemesh/p2p/server" "github.com/spacemeshos/go-spacemesh/sync2/rangesync" ) func makeFakeDispHandler(n int) rangesync.Handler { - return func(ctx context.Context, stream io.ReadWriter) error { + return func(ctx context.Context, _ p2p.Peer, stream io.ReadWriter) error { x := rangesync.KeyBytes(bytes.Repeat([]byte{byte(n)}, 32)) c := rangesync.StartWireConduit(ctx, stream, rangesync.DefaultConfig()) defer c.End() diff --git a/sync2/rangesync/p2p.go b/sync2/rangesync/p2p.go index dff29abbba..23746260b8 100644 --- a/sync2/rangesync/p2p.go +++ b/sync2/rangesync/p2p.go @@ -142,7 +142,7 @@ func (pss *PairwiseSetSyncer) Serve(ctx context.Context, stream io.ReadWriter, o } func (pss *PairwiseSetSyncer) Register(d *Dispatcher, os OrderedSet) { - d.Register(pss.name, func(ctx context.Context, s io.ReadWriter) error { + d.Register(pss.name, func(ctx context.Context, _ p2p.Peer, s io.ReadWriter) error { return pss.Serve(ctx, s, os) }) } diff --git a/sync2/rangesync/wire_conduit_test.go b/sync2/rangesync/wire_conduit_test.go index 435d84cfcd..4b3be3853d 100644 --- a/sync2/rangesync/wire_conduit_test.go +++ b/sync2/rangesync/wire_conduit_test.go @@ -72,7 +72,7 @@ func (fr *fakeRequester) Run(ctx context.Context) error { return nil case req = <-fr.reqCh: } - if err := fr.handler(ctx, req.initialRequest, req.stream); err != nil { + if err := fr.handler(ctx, p2p.Peer(""), req.initialRequest, req.stream); err != nil { assert.Fail(fr.t, "handler error: %v", err) } } @@ -138,7 +138,7 @@ func TestWireConduit(t *testing.T) { fp := rangesync.Fingerprint(hs[2][:12]) srv := newFakeRequester( t, "srv", - func(ctx context.Context, initialRequest []byte, stream io.ReadWriter) error { + func(ctx context.Context, _ p2p.Peer, initialRequest []byte, stream io.ReadWriter) error { require.Equal(t, []byte("hello"), initialRequest) c := rangesync.StartWireConduit(ctx, stream, rangesync.DefaultConfig()) defer c.Stop() @@ -235,7 +235,12 @@ func TestWireConduit_Limits(t *testing.T) { errCh := make(chan error) srv := newFakeRequester( t, "srv", - func(ctx context.Context, initialRequest []byte, stream io.ReadWriter) error { + func( + ctx context.Context, + _ p2p.Peer, + initialRequest []byte, + stream io.ReadWriter, + ) error { cfg := rangesync.DefaultConfig() cfg.TrafficLimit = tc.trafficLimit cfg.MessageLimit = tc.messageLimit @@ -297,7 +302,7 @@ func TestWireConduit_StopSend(t *testing.T) { started := make(chan struct{}) srv := newFakeRequester( t, "srv", - func(ctx context.Context, initialRequest []byte, stream io.ReadWriter) error { + func(ctx context.Context, _ p2p.Peer, initialRequest []byte, stream io.ReadWriter) error { close(started) // This will hang <-ctx.Done() From 9442698d1fd9d99bbb10e673732aa49ff79ff21c Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Thu, 31 Oct 2024 07:09:54 +0400 Subject: [PATCH 12/17] sync2: sqlstore: fix comment --- sync2/sqlstore/dbseq.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sync2/sqlstore/dbseq.go b/sync2/sqlstore/dbseq.go index f2d1c82dff..490cc75d76 100644 --- a/sync2/sqlstore/dbseq.go +++ b/sync2/sqlstore/dbseq.go @@ -138,11 +138,12 @@ func (s *dbSeq) load() error { } n := 0 - // if the chunk size was reduced due to a short chunk before wraparound, we need - // to extend it back + // make sure the chunk is large enough if cap(s.chunk) < s.chunkSize { s.chunk = make([]rangesync.KeyBytes, s.chunkSize) } else { + // if the chunk size was reduced due to a short chunk before wraparound, we need + // to extend it back s.chunk = s.chunk[:s.chunkSize] } key := dbIDKey{string(s.from), s.chunkSize} From 8a8c60cbd8ec0917a2dcb47a83266c372febe41a Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Fri, 1 Nov 2024 06:03:19 +0400 Subject: [PATCH 13/17] sync2: removed LRU cache It didn't add much to efficiency --- sync2/sqlstore/dbseq.go | 96 +++++++---------------------------- sync2/sqlstore/dbseq_test.go | 3 +- sync2/sqlstore/export_test.go | 1 - sync2/sqlstore/sqlidstore.go | 7 +-- 4 files changed, 22 insertions(+), 85 deletions(-) diff --git a/sync2/sqlstore/dbseq.go b/sync2/sqlstore/dbseq.go index 490cc75d76..c538441ce4 100644 --- a/sync2/sqlstore/dbseq.go +++ b/sync2/sqlstore/dbseq.go @@ -4,34 +4,10 @@ import ( "errors" "slices" - "github.com/hashicorp/golang-lru/v2/simplelru" - "github.com/spacemeshos/go-spacemesh/sql" "github.com/spacemeshos/go-spacemesh/sync2/rangesync" ) -// dbIDKey is a key for the LRU cache of ID chunks. -type dbIDKey struct { - //nolint:unused - id string - //nolint:unused - chunkSize int -} - -// LRU cache for ID chunks. -type lru = simplelru.LRU[dbIDKey, []rangesync.KeyBytes] - -const lruCacheSize = 1024 * 1024 - -// newLRU creates a new LRU cache for ID chunks. -func newLRU() *lru { - cache, err := simplelru.NewLRU[dbIDKey, []rangesync.KeyBytes](lruCacheSize, nil) - if err != nil { - panic("BUG: failed to create LRU cache: " + err.Error()) - } - return cache -} - // dbSeq represents a sequence of IDs from a database table. type dbSeq struct { // database @@ -56,8 +32,6 @@ type dbSeq struct { // true if there is only a single chunk in the sequence. // It is set after loading the initial chunk and finding that it's the only one. singleChunk bool - // LRU cache for ID chunks - cache *lru } // idsFromTable iterates over the id field values in a database table. @@ -68,7 +42,6 @@ func idsFromTable( ts int64, chunkSize int, maxChunkSize int, - lru *lru, ) rangesync.SeqResult { if from == nil { panic("BUG: makeDBIterator: nil from") @@ -94,7 +67,6 @@ func idsFromTable( keyLen: len(from), chunk: make([]rangesync.KeyBytes, maxChunkSize), singleChunk: false, - cache: lru, } if err = s.load(); err != nil { return @@ -107,27 +79,6 @@ func idsFromTable( } } -// loadCached loads a chunk of IDs from the LRU cache, -// if possible. -func (s *dbSeq) loadCached(key dbIDKey) (bool, int) { - if s.cache == nil { - return false, 0 - } - chunk, ok := s.cache.Get(key) - if !ok { - return false, 0 - } - - for n, id := range s.chunk[:len(chunk)] { - if id == nil { - id = make([]byte, s.keyLen) - s.chunk[n] = id - } - copy(id, chunk[n]) - } - return true, len(chunk) -} - // load makes sure the current chunk is loaded. func (s *dbSeq) load() error { s.pos = 0 @@ -146,38 +97,29 @@ func (s *dbSeq) load() error { // to extend it back s.chunk = s.chunk[:s.chunkSize] } - key := dbIDKey{string(s.from), s.chunkSize} + var ierr, err error - found, n := s.loadCached(key) - if !found { - dec := func(stmt *sql.Statement) bool { - if n >= len(s.chunk) { - ierr = errors.New("too many rows") - return false - } - // we reuse existing slices when possible for retrieving new IDs - id := s.chunk[n] - if id == nil { - id = make([]byte, s.keyLen) - s.chunk[n] = id - } - stmt.ColumnBytes(0, id) - n++ - return true - } - if s.ts <= 0 { - err = s.sts.LoadRange(s.db, s.from, s.chunkSize, dec) - } else { - err = s.sts.LoadRecent(s.db, s.from, s.chunkSize, s.ts, dec) + dec := func(stmt *sql.Statement) bool { + if n >= len(s.chunk) { + ierr = errors.New("too many rows") + return false } - if err == nil && ierr == nil && s.cache != nil { - cached := make([]rangesync.KeyBytes, n) - for n, id := range s.chunk[:n] { - cached[n] = slices.Clone(id) - } - s.cache.Add(key, cached) + // we reuse existing slices when possible for retrieving new IDs + id := s.chunk[n] + if id == nil { + id = make([]byte, s.keyLen) + s.chunk[n] = id } + stmt.ColumnBytes(0, id) + n++ + return true } + if s.ts <= 0 { + err = s.sts.LoadRange(s.db, s.from, s.chunkSize, dec) + } else { + err = s.sts.LoadRecent(s.db, s.from, s.chunkSize, s.ts, dec) + } + fromZero := s.from.IsZero() s.chunkSize = min(s.chunkSize*2, s.maxChunkSize) switch { diff --git a/sync2/sqlstore/dbseq_test.go b/sync2/sqlstore/dbseq_test.go index 87720007d6..94a9b830b1 100644 --- a/sync2/sqlstore/dbseq_test.go +++ b/sync2/sqlstore/dbseq_test.go @@ -220,7 +220,6 @@ func TestDBRangeIterator(t *testing.T) { t.Run("", func(t *testing.T) { db := sqlstore.CreateDB(t, 4) sqlstore.InsertDBItems(t, db, tc.items) - cache := sqlstore.NewLRU() st := &sqlstore.SyncedTable{ TableName: "foo", IDColumn: "id", @@ -230,7 +229,7 @@ func TestDBRangeIterator(t *testing.T) { for startChunkSize := 1; startChunkSize < 12; startChunkSize++ { for maxChunkSize := 1; maxChunkSize < 12; maxChunkSize++ { sr := sqlstore.IDSFromTable(db, sts, tc.from, -1, - startChunkSize, maxChunkSize, cache) + startChunkSize, maxChunkSize) // when there are no items, errEmptySet is returned for range 3 { // make sure the sequence is reusable var collected []rangesync.KeyBytes diff --git a/sync2/sqlstore/export_test.go b/sync2/sqlstore/export_test.go index 6c0c5c9167..bc3ed7026b 100644 --- a/sync2/sqlstore/export_test.go +++ b/sync2/sqlstore/export_test.go @@ -3,7 +3,6 @@ package sqlstore import "github.com/spacemeshos/go-spacemesh/sql/expr" var ( - NewLRU = newLRU IDSFromTable = idsFromTable ) diff --git a/sync2/sqlstore/sqlidstore.go b/sync2/sqlstore/sqlidstore.go index bccdb97d54..e26fa37ad7 100644 --- a/sync2/sqlstore/sqlidstore.go +++ b/sync2/sqlstore/sqlidstore.go @@ -13,7 +13,6 @@ type SQLIDStore struct { db sql.Executor sts *SyncedTableSnapshot keyLen int - cache *lru } var _ IDStore = &SQLIDStore{} @@ -24,7 +23,6 @@ func NewSQLIDStore(db sql.Executor, sts *SyncedTableSnapshot, keyLen int) *SQLID db: db, sts: sts, keyLen: keyLen, - cache: newLRU(), } } @@ -54,7 +52,7 @@ func (s *SQLIDStore) From(from rangesync.KeyBytes, sizeHint int) rangesync.SeqRe if len(from) != s.keyLen { panic("BUG: invalid key length") } - return idsFromTable(s.db, s.sts, from, -1, sizeHint, sqlMaxChunkSize, s.cache) + return idsFromTable(s.db, s.sts, from, -1, sizeHint, sqlMaxChunkSize) } // Since returns IDs in the store starting from the given key and timestamp. @@ -69,13 +67,12 @@ func (s *SQLIDStore) Since(from rangesync.KeyBytes, since int64) (rangesync.SeqR if count == 0 { return rangesync.EmptySeqResult(), 0 } - return idsFromTable(s.db, s.sts, from, since, 1, sqlMaxChunkSize, nil), count + return idsFromTable(s.db, s.sts, from, since, 1, sqlMaxChunkSize), count } // Sets the table snapshot to use for the store. func (s *SQLIDStore) SetSnapshot(sts *SyncedTableSnapshot) { s.sts = sts - s.cache.Purge() } // Release is a no-op for SQLIDStore. From 9d6441f800bbc6c31ca86b935ba9f1b6d6eb74a2 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Fri, 1 Nov 2024 06:07:18 +0400 Subject: [PATCH 14/17] sync2: sqlstore: fix comments --- sync2/sqlstore/dbseq.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sync2/sqlstore/dbseq.go b/sync2/sqlstore/dbseq.go index c538441ce4..e2ae77cfad 100644 --- a/sync2/sqlstore/dbseq.go +++ b/sync2/sqlstore/dbseq.go @@ -123,8 +123,10 @@ func (s *dbSeq) load() error { fromZero := s.from.IsZero() s.chunkSize = min(s.chunkSize*2, s.maxChunkSize) switch { - case err != nil || ierr != nil: - return errors.Join(ierr, err) + case ierr != nil: + return ierr + case err != nil: + return err case n == 0: // empty chunk if fromZero { From 6424acabb94b99f1696094c03e607831c099dc7b Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Fri, 1 Nov 2024 06:19:52 +0400 Subject: [PATCH 15/17] sync2: fix lint issue --- sync2/sqlstore/export_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sync2/sqlstore/export_test.go b/sync2/sqlstore/export_test.go index bc3ed7026b..6896fc2d2e 100644 --- a/sync2/sqlstore/export_test.go +++ b/sync2/sqlstore/export_test.go @@ -2,9 +2,7 @@ package sqlstore import "github.com/spacemeshos/go-spacemesh/sql/expr" -var ( - IDSFromTable = idsFromTable -) +var IDSFromTable = idsFromTable func (st *SyncedTable) GenSelectAll() expr.Statement { return st.genSelectAll() } func (st *SyncedTable) GenCount() expr.Statement { return st.genCount() } From 79a31b774817889a02f2d64da003f9098dd4477a Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Sat, 2 Nov 2024 05:17:47 +0400 Subject: [PATCH 16/17] sync2: sqlstore: cache generated SQL Re-generating SQL statements every time is quick but increases GC pressure. --- sync2/sqlstore/syncedtable.go | 85 ++++++++++++++++-------------- sync2/sqlstore/syncedtable_test.go | 6 +-- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/sync2/sqlstore/syncedtable.go b/sync2/sqlstore/syncedtable.go index 04b6a6cefc..0d6c252a21 100644 --- a/sync2/sqlstore/syncedtable.go +++ b/sync2/sqlstore/syncedtable.go @@ -23,7 +23,37 @@ type SyncedTable struct { // The filter expression. Filter expr.Expr // The binder function for the bind parameters appearing in the filter expression. - Binder Binder + Binder Binder + queries map[string]string +} + +func (st *SyncedTable) cacheQuery(name string, gen func() expr.Statement) string { + s, ok := st.queries[name] + if ok { + return s + } + if st.queries == nil { + st.queries = make(map[string]string) + } + s = gen().String() + st.queries[name] = s + return s +} + +func (st *SyncedTable) exec( + db sql.Executor, + name string, + gen func() expr.Statement, + enc sql.Encoder, + dec sql.Decoder, +) error { + _, err := db.Exec(st.cacheQuery(name, gen), func(stmt *sql.Statement) { + if st.Binder != nil { + st.Binder(stmt) + } + enc(stmt) + }, dec) + return err } // genSelectMaxRowID generates a SELECT statement that returns the maximum rowid in the @@ -118,7 +148,7 @@ func (st *SyncedTable) genSelectRecent() expr.Statement { // loadMaxRowID returns the max rowid in the table. func (st *SyncedTable) loadMaxRowID(db sql.Executor) (maxRowID int64, err error) { nRows, err := db.Exec( - st.genSelectMaxRowID().String(), nil, + st.cacheQuery("selectMaxRowID", st.genSelectMaxRowID), nil, func(st *sql.Statement) bool { maxRowID = st.ColumnInt64(0) return true @@ -153,16 +183,9 @@ func (sts *SyncedTableSnapshot) Load( db sql.Executor, dec func(stmt *sql.Statement) bool, ) error { - _, err := db.Exec( - sts.genSelectAll().String(), - func(stmt *sql.Statement) { - if sts.Binder != nil { - sts.Binder(stmt) - } - stmt.BindInt64(stmt.BindParamCount(), sts.maxRowID) - }, - dec) - return err + return sts.exec(db, "selectAll", sts.genSelectAll, func(stmt *sql.Statement) { + stmt.BindInt64(stmt.BindParamCount(), sts.maxRowID) + }, dec) } // LoadCount returns the number of rows in the snapshot. @@ -170,12 +193,9 @@ func (sts *SyncedTableSnapshot) LoadCount( db sql.Executor, ) (int, error) { var count int - _, err := db.Exec( - sts.genCount().String(), + err := sts.exec( + db, "count", sts.genCount, func(stmt *sql.Statement) { - if sts.Binder != nil { - sts.Binder(stmt) - } stmt.BindInt64(stmt.BindParamCount(), sts.maxRowID) }, func(stmt *sql.Statement) bool { @@ -191,18 +211,14 @@ func (sts *SyncedTableSnapshot) LoadSinceSnapshot( prev *SyncedTableSnapshot, dec func(stmt *sql.Statement) bool, ) error { - _, err := db.Exec( - sts.genSelectAllSinceSnapshot().String(), + return sts.exec( + db, "selectAllSinceSnapshot", sts.genSelectAllSinceSnapshot, func(stmt *sql.Statement) { - if sts.Binder != nil { - sts.Binder(stmt) - } nParams := stmt.BindParamCount() stmt.BindInt64(nParams-1, prev.maxRowID+1) stmt.BindInt64(nParams, sts.maxRowID) }, dec) - return err } // LoadRange loads ids starting from the specified one. @@ -213,19 +229,15 @@ func (sts *SyncedTableSnapshot) LoadRange( limit int, dec func(stmt *sql.Statement) bool, ) error { - _, err := db.Exec( - sts.genSelectRange().String(), + return sts.exec( + db, "selectRange", sts.genSelectRange, func(stmt *sql.Statement) { - if sts.Binder != nil { - sts.Binder(stmt) - } nParams := stmt.BindParamCount() stmt.BindBytes(nParams-2, fromID) stmt.BindInt64(nParams-1, sts.maxRowID) stmt.BindInt64(nParams, int64(limit)) }, dec) - return err } var errNoTimestampColumn = errors.New("no timestamp column") @@ -239,12 +251,9 @@ func (sts *SyncedTableSnapshot) LoadRecentCount( return 0, errNoTimestampColumn } var count int - _, err := db.Exec( - sts.genRecentCount().String(), + err := sts.exec( + db, "genRecentCount", sts.genRecentCount, func(stmt *sql.Statement) { - if sts.Binder != nil { - sts.Binder(stmt) - } nParams := stmt.BindParamCount() stmt.BindInt64(nParams-1, sts.maxRowID) stmt.BindInt64(nParams, since) @@ -267,12 +276,9 @@ func (sts *SyncedTableSnapshot) LoadRecent( if sts.TimestampColumn == "" { return errNoTimestampColumn } - _, err := db.Exec( - sts.genSelectRecent().String(), + return sts.exec( + db, "selectRecent", sts.genSelectRecent, func(stmt *sql.Statement) { - if sts.Binder != nil { - sts.Binder(stmt) - } nParams := stmt.BindParamCount() stmt.BindBytes(nParams-3, fromID) stmt.BindInt64(nParams-2, sts.maxRowID) @@ -280,5 +286,4 @@ func (sts *SyncedTableSnapshot) LoadRecent( stmt.BindInt64(nParams, int64(limit)) }, dec) - return err } diff --git a/sync2/sqlstore/syncedtable_test.go b/sync2/sqlstore/syncedtable_test.go index 4e9fa62574..28a944ab35 100644 --- a/sync2/sqlstore/syncedtable_test.go +++ b/sync2/sqlstore/syncedtable_test.go @@ -15,7 +15,7 @@ import ( func TestSyncedTable_GenSQL(t *testing.T) { for _, tc := range []struct { name string - st sqlstore.SyncedTable + st *sqlstore.SyncedTable all string count string maxRowID string @@ -25,7 +25,7 @@ func TestSyncedTable_GenSQL(t *testing.T) { }{ { name: "no filter", - st: sqlstore.SyncedTable{ + st: &sqlstore.SyncedTable{ TableName: "atxs", IDColumn: "id", TimestampColumn: "received", @@ -42,7 +42,7 @@ func TestSyncedTable_GenSQL(t *testing.T) { }, { name: "filter", - st: sqlstore.SyncedTable{ + st: &sqlstore.SyncedTable{ TableName: "atxs", IDColumn: "id", Filter: expr.MustParse("epoch = ?"), From 59f7db5819e5b482227a409a059b16a501d1a2b7 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Sat, 2 Nov 2024 05:18:26 +0400 Subject: [PATCH 17/17] sync2: sqlstore: reduce memory use in db sequences --- sync2/sqlstore/dbseq.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sync2/sqlstore/dbseq.go b/sync2/sqlstore/dbseq.go index e2ae77cfad..bdcadddca0 100644 --- a/sync2/sqlstore/dbseq.go +++ b/sync2/sqlstore/dbseq.go @@ -65,7 +65,7 @@ func idsFromTable( ts: ts, maxChunkSize: maxChunkSize, keyLen: len(from), - chunk: make([]rangesync.KeyBytes, maxChunkSize), + chunk: make([]rangesync.KeyBytes, 1), singleChunk: false, } if err = s.load(); err != nil {

(l+h%`v63#qB4D-lD-iEi=ONk|`a@g&Sle6L~T`^oDG zHSPAOF|wcIkOD;Qx=F;y%`j#Fp3U)7or_0=MScyK;joOZJhGFbn8EV1eZNOV+`nS&q2d4j> za%Lt+4e+Y?2scRwR7pMyofVD4>xgvT5xEUaSLige%!)cQyBN#w6maceaM2$3>NRZO zJnt=}@p^hMUJ(&w>G2qsptOIWt%0ZLj^}5_^cg%WOn(r8c%LY7De66Et{TWg3j5)x zwU4cPe)G*W4rw363)x`#x-PqlvI!#t$8F(JilnEvRq++apf*+v|>k=LK7&z6T|qkac`3J=s>&q*X{RXch^@Fte;y%6`LE z4)ck62B(uDg{abp!5oz$h?y~Euzuny70)0hNz~}EUsw#G3#EG0f)ZTE+6aKUG6=ju zUPggXc(&MNgNyZ(7nOMeSHRSYc|`EM;xd1~0}<#l!hsV4!3ZpRqJtzLT=@kjP5wQn z)=QDXVUPg2;<8=oqn3aal2b#oHvn)D)}<(LQoFCKNDz6@l@tup2G}?Lk)$eXf-8^X z*mt$>wM=`GP^K%%O+7mLh@TIFh@zuDd*4ySk-OQm4vUbhivTE0q*8Zo|D={;NIyD# zqBmOC7cwJYHW&@c zRin`M4y2f4`gkp8A*8B|7x3GC^AsxzXh=n7Vl0!Uv%YPQNx z?2pA@5}-s7{J0~Yci2u_KZN0!U|d6dM4SH?OJ!D>q+n`1js+53;+Ff1d4e=NG~?gM|S{rKx1(-YRr zSKKgd%6F3-S@EsghBH&TTrCpnYYO|K??Q4ch6KGe`YYuAm55Vw|0fvkc|h8Qe@e18 z1$Hn^rZ82xKTC(QHG17W{bUBKVPKDS$E`&4mu*XWURU+@52Q^%*bGi&(e5RpMRxHN z)`?w#^^2pV5TwXXTIx;0dnlhJoV3y>1Y713mQ&FXqet#m#nNx<|!FE-YF)2~-j^)(D9O2$@lfHy?dnhq4xWc1^bd19^<( zvkkJQEvV))qX*7onm)|`(ubQOPPhG3_(CL_--23DIMDMEnk6 zvyAChJmM6+Q7mjdHbE0Al{pZOfV{*%5MC6)INdi>l?6p%hOLxp$go1YNO`>0 zGy;!cs%12ICKDV9i2N!MfEcicL^Y6=MG<^68SZtyNAre6LT4}ex#n!)wxi2V~%{-0#3KdRf)p=I}k)X80D6!C9*S_`wk3ssO zxM<$s6gL-C_dCJ*{$Fx-O+efTH{h6c=f6lhtz8UY8xp#p~odc5OnC%b!f0Zk6p~q?aFCRP?YS^E3y4Iko zJ>dB=7@=Dkm!RnsdYj*d%(mQgXs<0ZPP&1i);zp_AT{r6G3!Mdp{xn=c}#PMR28#S zE<~WWuUG8Q;%}xQ@ZeStI&lL?D1?PM^{*yB8seXEwe};By7YGhHXGGGj^9HcQ4MIy$p)6VM2zE$jgklIC+H*w$&%LbE&JYhtWt@gIwt;F~|@TY+Fc_>7b+(CBx1 z+(cKuiowr@c3uFuY+9A7j{yVvBha84e#-Z5gZZjZGzD)UXbY!0xW(^J16_-$rrx`P zT+jY`nA_^I!JbaBk?9x^_F=HUU@B9Hrn;UMdo#&4xI`}qTV(SC;pO%52#UMTFoI+6 z8q8)va&(WOXfNKgmp+GR$Qy$FGSe!Mhn@h5=RSWM@^&!~YiYnv0+jy9gVy_z?JYjR znMCtoXFDMVLb&kp;e7UEogMx+;lJ4~AFIfDaZpQvuUAt1CtjroeV&V%PR;0b6ghM6 zn))e-EY7sHAJt^_-=Jdu3VhlTqt}-kBjs<*wN6qZ@j`&gzgesQ(W|6h+%0Afo7~Nq zJG9E2vh?>s+O%}vp7tM@CDDz-MVBC25FX9p8o`)X)&_0zR+r&gAHv?6Qppu?-^J(J zLk*;(j!d^mP%OSE*q*V=g}5ykN$~sL==K+;AJ)H=nZZulj|;)Fc*!ZP9%Mv@ofO^P zcMMMb0wKsxm+I3rl_pSjQa)6diqmls%0B_5WG~+8;^HbuaX4{%E-wYHF}2VJW3YUY zQo^BB>CzK=@ApKUPQ$ofy@QKZVtsLczopEOf$rF)lcH&^oQ=uDWvA=toH%mJM^SY3^9v0rf=v zY}Ks-&!Xesv#(*Wm*~g0((n5ZRc}p9>8{U>Z+zC|rc9-sme6m$2V_5<7S+$b@%(P|PkfZb$&VVm;EPgBD8P4x6<% zBoLhp({ZR7h=eEEz^u9gUULBPbq9hb^@4c^9GM}62)9!pcRJKjjD{`l0(eJ}C1DmQ z>$oagAm(2Dh*q&-! z?bur{uP4{`fI3Th#6s&kuVU&-8-KEM?U(GvS-nxE{TfVMu}YoP=RH5>en}Ru^07Uj z)a;M(EYf&in{BBrf~#0o?-$hy-+S$P^|$Av<@7lcb)a`7b>23#R(jUyySvcmw(Gnx z|L|c2zjr;Rv|ZJ(=7ZYW$F7VY=l<@w)#VGIg*LfJJ@uUKw#=Po%amyyh~qho&E$B=V9p7Ln3^AY7JwSmV$ z0*0|z;zd7^)7a-~b4@6eD?SieXlKIE0qv1<0qS&l-nbCIep%R*cp6QQ=)bbtxYK*= zoD8hBd5at}(ou=F#yb{=o+2UfY?tE2U?bl_Me4l(Vby~LF>Ei_QDi_{ zJ(cReo&b^^)LTw33wbm8{bDGFy>aT>235j|qbaV+eh|9034rTJ%f=EV z6MxB>ba0#i)`>yn`XKqt4vGr1SEn99pVjd4!-(1t6lG`=!IU3F6}k*96uQ@sY}`Wz za|9jcv-pMny~UkpJm3iEQU-_d8_Vbosl$g2j!Uxgv2+y}LCvbbA+z!Ax9Ku5f5#a~ zz<0p8|DmHHX({@0?vwD;+2(cpaeznk-@dO9+Q_Uhvwg)1!N-b_7f}XoQ86J{37V%; z4r{ufOmu}D3=PMNQ6za><>2(;WyGkip>>ay&yy2^#w{(oYM8}G&?SUwR*8J#k(yDL z%4@Ab`pvrOv=p^D!y|34n2b`UDR5-#&*WmOz$x_IczZ-!Bay9VcC>qS*=EUKj z{vsR2u*wt-0{%%@aQ$Cdq0{ZjIEB^;hMrgf<%l7i<>0fxhDCmsP0QZ)dcml#GH401 zRXWd|M_f*O7Eb8SMtM-X%K5Q`a|+|xLX8qmtJdW`1122@`riwSkQkcPP9~)Z;ytk9 zb9sn+5LyA) z744?W&%|9|4F6Fzyt6ob#dY(J99H&^pC=a23L&2i<(VlkHT`LCdAVFrYY2R$;m6M6 ziFl{yb7w$a%kCB&1ImR=zjD6M!v6Wen?Uei{+AIMT4pZyuV183#QE0KAIJJ`z^DX2 zamZ!jd(&0JG@nV`8JvRFkt+NV`iKSH@pqeYE-EyFfYn!H`A=)yb`8}J2^w8A-NMX8 z_TlmPgpk0`Y5j&DWYO?36?5lBYurTaqT~&DArJ21KjqU^qE|%R#8Tb7JliiVMb7CL z?(qR!@g(@$gN(5fvH)|KK#Q#n0eI2Xbx}S@jUZ@aRxw3wod=EeDj9F&z{+Wd zsGL3l$i;EM=u0}6I25tB1MWwo3;e(<@%GOUNZ@eyd-Q;)=@aj&Y3ijfpoK3(+Wl}I z`*j|uHGjh1bxh8u2FVXJ7|kt=GCF-iUFV0zAq<(g zyO67+`ZjNLKi&i`|#6uD{vz8v>o`xUl6IOI7SGhJMld zlifFf)%CSa72!KH4Ekk%yyZA> z5;iu4bfh{?V>a`srd^&dUv_8gCAfCD{pxF6o=HZ2_ofhs(oiZYva;yx~G zMlUus!a!jy#62u42Ctyy)FncB4OP_@=LbN&!ZWIU zly&A&x^vGJ8jd&{@^mWeP$qew5*OD#`m$j6fW45CFioKFNtEvg4*!2k7+#>mWdqE2 zrMf!;k3iT;8>9s}uNvL=?WcQszyQ#9ToT%1KY0dl0QB_=l(wW zcrcCu1|70UNd#%j;^u8r$ts9d&HnbS*IV?Wd%U=DE5@kbPv2G0oFw43^J44AhoH7M zCVyDX_h8|R07kyD!}{0z%jamU56;H3RwU_tLsutWHnah#v@1miFb^tv&%S4WeMxV7 zTLAc~Oy>EXev6E-Jh|gf7@uRBmAuo>RiZBsJ1(21afPnG^A4%DxgFCU)Z%(vI#OCWC+IZ&9Mq&<>ZN2C9ubWwI~%@uWg6f5nZjo*Wb z%JQI?Gh;cpb7Y!@tiGgiuabZ zJs&0NRG?qj%te}OBVZY*mEhnZ^~%;BQt{oYTw3j3bbIl9g@a9kI6Zne zFBO6lE8+CifNW7#F+}yHdj@`1!;@hQ9_4OfeVud()z)kzCEb^VY~|{A{%d+| z0h{GY7q(?>#f8JYFN6^Z@(y3m8-;#4A4ov*a4nWkVbsEu0o^5-qg~UIgP{+Tp7+mN zDsAVQj1Kyu1W~|oJ1N3teoFr%h_e3n-r`Z^c*)MN!IdaQzR*=ukzvQlWX#vuf3RZj z|5#k|3;&bSRFxt7{kZK{dpoyFwbOT>hjvpoF27+sJPG*MVS0v?PV3B;@ElJt$+~zw zT`HWvhXmjb!oV%WyMVHBKw|#>MhDE0FM54ZZ@?DJ@LDzmJ4DY~f}9KqCUx=?@{>+w zC|W?Cl?Ci0KI>AO$r2)&o{q=cV+CQ(DS{(cAU4JHHIJef)dme}T+Lu(2zb3N+U@Tu z>%!rrPD3dA`TSi`822@%sPdj0AHqB!f>u7i>PZ zF)`u3&N5_3&Pi?FmA>$~c>(eM1Lc2Bz%oF(!=YK4?00u ze26;o+FSsD=s30!X`1Ugnf;$&2X+t_TS#GXu*aPpvSuq*;Q9l*$5ji+Sy`nxd6dbv z84ZCwt5tkh{8u{!YgLfelgz3xzmo};mH!D(%_f*p()@m`7$a%hd=)`7=+Zpx0fJq* zD{166W+z2}fP{mDQ*N{rH+=!=?4;Hgac8^!7ggeFO zfg+%65t5~;>LOfR(MQ!QTPPDU@zPG#0IgUXMUt1S{?|W;V>*Fd$~%rI(-WM_-PLUv zu1O}?yWl*T7V_GUQA0pdPW@`6_ zK;AhkNkcLO(bFdIie=?Av{n`WL%0?`gdib9$H zNj5&eF9KwY(vz~vTCp!C#@1uQSoW(xviOalSv6mr!^8dgzv-LX#mWgzlt?u@)WoF6 zam){_D|wDeQ~oEkT;|$rGq@+Z@D_3j+~CIAcsN z#7#-&H9BgD`pYOS_%52^YF#2#$x?Y z@gutW+r4d03p)>Z+E3>lJr=tYb-D)(zEqjzT(FnWa$3{p<&ujr7+F^G(9>_zb7>(q zkZl_w((0WBa>2o1g$*LQgq;qYKlrFpZ5B z7#iSuUT*B;%94H}OV%FsK!NlpArcK++HS;y9z<_aA`eW_4TfHzau|r^gX)OCU9b!y zW_5IJ_s(9}?x*!a^r*et>fD$EdjD9j3t6joho#1>@Ii zQwi^8$f9iEqmyL~txQn))^zAYe!rv^|26Q`K}hW+^=X5{SZ(#B-;h4@M3@#~)vJQ( zV+AW;%FjAdYoh>SKj_9cD;p93 zcmzQ9c@h~dAr0+FaXT2-EO+(4hH zfIOs@f>Mw=d)yA;_{D!4efzt&$p!yMuc|_Sxf;S&8H5Jiw!bE7z_fNel-46e2}JPZ z8yq{7A5cZ2dk=|@$>GgcTNC&KvRIAlrnZ>J0sWum$9I}b75$in;Khqo*@jc)gID~v zYTNgf2Rz9DSW@r{F7qOD#NG`gNdF2dP8kXQI`%;h!w@t^)`S9&(s<>o&cds>e;}CQ z^cu$n9nwHQND>L0R38EdWkp-L3cx$NxB#p2u5V4fJU4KjWfy-2QGSrFosjv`)gL%% z|3aq)pP=+LkL&-z-$f!Mp$SuLc54Q>qjUtFBact5*Gbw ze~2XAUC37SOY$^Xi+cI4No9OnQiWR)d*UH=_LYb7hW`H(d3)fx+Y5oNNWvuA`HwaU z@)7k5BWLvwdXq<=7Wj0IyFZa(Q^@(H=Ryb}v}>KpPa?u5y%yd&3Vaz=8$RenqcT1? zapqjg;FO!adkGQXU!#gLsXrSWaT$N2ymEy0Mv?zkn0Llk%zc&Hh@P~M0zhIF7TQsc zRTd&2g2Z~$oN!+zYocVHdJXs-s2F>#UP1yks+pg4ttr((A5rzZ$h%f@QNjt0FF~@s z644+(Eys}m73BE%dw087p;1l4I<%6(2Y`ZWfMzk%;KFLsKrBHeX42@Y3n&01uJ=I) zl+K{qK95B(*hzssO|(=<`l%c*JL+0$RL8aZr|CDJ!W2Y~5YBQ7VXja0JM1P;sd_zL zxCG}_F(ahrL*!b?V|_=8eLf2qq>bavmp^XZb9R4s%HRP<7wKrK=8z2DdPj6m>*kT9 zhvnOpPV;1cdjFQdhCt*mQtcVnFgrd}3L&Oc<=_)!@WHg)Qe4f~?`4CJMcws{{k#q5GS|KE`9W#UAdYQ|ojVBR~` zE$s|v3&q#JcVkv2;H1yyO4tuGFuKU69fLBSR~QPuJznWw{>Iho=d02^3vFPHO;^1wg9YZB0UEYM4n6tzMIV=5-@;R0Fb5J$HB z!@T+?C-1{JFL4ElxDZJUs^no=U7;Fu>M9%IyW8_F9CZ4G%^1=XMssVK1D_u|H?B)t zFUJ)~`!X`_J=oO;v1ge)|AQw$b%oeN;^^0llptXwCQr0PZv` z-&wg!(krt*CAmhz)21z;ddIgOCUjChB6yU1!NU&^d?}$KmW%C3XpUb&ShWDSl&k;R*TJx;wV`kC){adD;a=X@eO&~xWIms!ejij%;Jypv|iN?3- z@uI?umVu^``oAs+C0T*%N3TOQw9if+Q%=Y|7V|tr=bZLSIm>Pxpmc7ZL-NACLtrz zNEfdsD){7x0VO}-369HCnAUgKJgz;p`|lSKk^_XqPpo>)^YoT`q6+{?eFBVM6fH&$ zeqRHFl@3thO8sswCqd8l_gWzbFF30GQOy@NZW#&x615Iy{zg;PJk$s+U^u-DOO~&=R7ox-dVZF zHXF15V*~tM=&kbhu5t|CE4*hb7fa-2e52e^HPU#eEUa*Ex!Z$A%g0|m1!X$0xcUxm z;(JfT3B5VTKc|WG!p>DdLk-fTH(6nRU-&X`Qy7l@<+E64>#OtnZ3;~#D+r1RuYLU9 z#Aj8jH`C?sloqK@mZQT+Hv2OgEdE3p8W!FVt3E4m@(uX#-=6huNRP$Hjhfj?WfvR4 zMIlY$2^zQ#f=zppF}eus9>p=Gjog2`S;Z|7HPh$q{|=X51Kv>$;Hp-MlYv|&J2MlL z<`=_nB>?_?|4F&Rm=p!xBw$=tdrcev`aYDbb*ffKfL}|mcodG_5DbhL34f+5HOF{}&-;UeU%4ZkJQGJl`Jc1ETa z^%VoA5W)E?+>AI#D+^by(R7(q-^!OgjW-~QpqPLq3+w-MM;^8dRW z;pg8^8f`YRU%!&lnYe7AMIkJqgi7R3M!hUIw)3YYB`04W_(92vS``@Jz0I-f$Q4;# zZI-{mgz6mCKEmd=fU%dEgVy%2bJ&p~P79?;*86zMFv*vxrco&Xf{KfxaeD;6IB6O_ znq&RES{ZZNJ#E#;7QDocf|Kvzp~DLo3Wdqi`jg#kDqd_i4O*Oj#BTUoy!#YQ3-yjf zb5y$Yt0kqvPB=2}9m-U-kAI`5L#MZ;AL#WYRhx!u$L}1R{B=tySHr39vH9ay1u#&#Noos zEl6gR^v0ZeuZ5{KF6|DS?n$85VsDF)q|5O8Yl|k0XGsHbg$}M zu+CH^*@sMOpXOwJm9_G#*1B|m%hV?ML1&8DDNxUu5|uvA426|u#`yO1@JT`P$ndt3 zAFU}dE*k|QCIlHLmlU2iD@jNXfnr*SBbnbehp=JIuCHWMGCpFK{fN^b|{ zWl?+9+Tm4pnHRx&H3$Xfx|v zU$=jPwoJc%l$bOL$tF?ptvF*AMh3J>ol17?ma?Tbs}x1P{js$>-_o=sIXF+nZL@4C zLD^zi7)9jhTKO z{xb>4uSPAJhJPh)Kadbq6h*}E@;;f6hkL~TJrsh_2Oml>=h<;FJ#Fs)bdNB#>Tyt% z=+ljk!OBD(MI|_}OF($)cLW;$x>s)G$(bj`JMo0)=)tnfCt`-08n8>${njYVHM+HV zPL~NoFT4tn?Lgg}M=<8oT(@kVSAi#6KiTb9ejB@}bqNwkUpvYh@)4g%dIJYovfjJB zxduLURPeylL48N3L8kfoH?LO`FAC7t1!kR8&7-N$jA%)v#3<>kc{o)L z{w?adY@a?jY@Ch=8K>uxdx4i?GO9g=&EH`X-#2&jM~(gPll3>4sKMa92vB9}UL~!ct*L!ZlEa`QENr4qg zf&;xeU1McXCJeDCDk^fGbL#B}+t{ehvGY&&pDhNWseS)D@2}*X>SCKr0P;~crci}z zBr(GkwTH!`PRVIK_@}#=ZaRt7^%w*o1gJHjHtE@$}L{$SAQ)XNG5>`q$UmKLAFA-7;|H!5h0OyOvpgdtZ5 zf1InI%dUYTd!i2SH`LI8)Qyv-{Z_ef;xtMt`f1fg@HvXwXl=>Xmq&7_PW8$~{5^|H z2$9PlBm`G`e?5d7vqkaOyZNE7_AM{tP1sjBAsC9tbW?N`WEQig_~>$M9AWhBAHwOi z9kq^jM(`bofXm7fVlquI4uF*lk4ffwY=i>QM**tgBtU1owzjsJHDxWAIsvti>R$7) zIwK8Z9oE0sZ)uIfiECJ^L9m*dffQ`74Zc4jZE@kQUkts`!va!&953A6y1yvN5+|j2jC!rqew)c9E&Y=3F{#D_ z=8D{amRt>0J%SQ)l>fUr8&q8$!&wzUs$Gjkw$}%K#Uo zL}HVn#ErD@lGpnEcQ5aKvkYF_)t`L}AaaIY`FfW8}LXL}YgBGuENNq`>|IGXEdnv@-%I>-DJFt2%Cts)I5|(!>x&5s+ zk7aq=lQye_U`YW=)Epm+tYILpj z;zb%S>RGL^3{n0=|Leml5Md5_3%*#Z0MryOA0qt(lTz3|ORIR~&wV7bka_liIKN(! zo2f~1D+q%6mo;yndk^7Yv@yb%lZo2sx=B1AY7NR=k|Fnl?b4Ex`$}Hjk3?XB9amWd z5r=4-O3*Jy^m;m{vX?q?lf*JsnA9iV;%aW(|NmX>{e}NaZ+A(heI(tmt~0Iwm3?lQ zm$6Za);spXb6~?L0}I?oe#Z@vbF!}tbuf?zA-Cs)9Ofll?J(ro^-8?typ5Cd>bz)oc%22*!bw--O zT;L(JDLaHh*DK}v1scgxX%nREnP+u^+Fq_@O%*jzfwyKGXO&-BHJYdXY5g>+hk72sID+*G840 zj6*>jgc8es$ZE&N@VSRyVRCRxu0bXB?8+%<4LpH$1k_JxJZDnVOpCRT<*`o@r)9*6 ztzTe}LY343fSA|X`NKNRITFvE$1a;F2)RhXbs+vi*2fdy_o}|aeB8q1?ku52`=RBK z=pa&+C&*>nH&lj53-op2SIkOo_IGhvTfg34dLD9!4%1a1d!(lsgb)?#;~hzcy1)U>3&n{rx@peya|RfK?X@Gmnm9>ti0~EXa@Zn7fqW z_6R7AZ-1*jN6p8@C17&`mjfrSVG$&tsADLiW+H#&KThJIcX&sfG&_)lRf@4mmfa%! z&l~-DIay{urI?hD9AEHAo7>yZDYJ0N4^(*+Vj`#^_}x?f?)5g_zJ?Rl6Gwta7SF;} z@Qu9l<^H+1qZCA`KfD!pVD^0FM@vFMR(ai0yF?tW_#h6hrE<8Q4L7#4jdoO>FDAwX z7Pg69gc|sUw!?tM6O{^g4_o$(omfe)l1}y$ki0mdeV{3xzQW)hB6u_~iVS5aM@BBa zJ8!hBs=9+@N$=E+;pz$44VPi@VVF6$?4b20Ng0W>GApA0_9uG@P7-*H5A8UTkRO=i zVXnNoX0q1iddc2=tK%AMTShWDXUqUySoa8>q-UUkNG+@dbp)#uZ%JT`Jd2~p9mUD` z9+2X2e)FOEz7Y@d5ZjT8-b5B8<(K~TnNab1RU?E!E}$t!ImFrxt;di3YE-`XELHDi zvF(sZ8r}#+IAUB$e#h%S^_&H%p;pgRkPI2fQsK#9vu#2vXrYzTEjS$;f_QK zp}6EY&zuR%Y`=Jx0>g2Uv`W5bV~kZ-OsE1J>463A8)LI7MC$^_!5L>4v=*y;=aFt3 zJmf~xIDOzJu?-8`SR87FvUv(vVh=%1W6_#FV@W}j@4Ni5hk~Glh&A4@cF9T#w*EG3 zD5m5Mm`j+zVusSncoOn?wWnv_C|?Ze%)Xk3z43dqc2^-I3Vv%wavhSBbY}KFzD^vZ z4C%sjf|L5YGU$nLaw#&x^UX5m-}p%FF>#Mw;xv8{QG?mL=gEo@C>Gg&IEei}Mhd{= zYXv_J6;w$9^^D^8dv+fSA%9G#(;GK6-+K20_||WSmXRP#Jmh|p^0cru3sO_ufnDd0 zu(n|gvmUOSsXB*O`Lt49vq^wAZ@e zFy*tdJB4vE)`1aWjFDO>m^bktvlapuG3*wX!Go5e670vO#JLc}0e37+nH>r;WhsB~ zt(vY&!?U_8<8J~ayAMY-k4pGIRXmFG?^Aih2+mW~sYO{I4gKmb1uQ0R;|&cBijqf; z90^`L_oc7)cku~&sbQznbQX>PkGH}STTT$Rz8;~Rx>XlT0XdZo?roXq1OzNYLa(}h>H{1#sTs9Eiv?V?{!%nlZ3TN)CPbf%(B zD5wtqGbAHd2z?K(5W`0{vzIK!Uc4$j%%PqqZijfT&y`;qduQ2}K;?s42NlT5+Fi#U zj8Y4XaBMK_+4y*9M%fq_BJ)%|EpOR4ic08aq&Vki zcc5xbKB@in42Hd_6NoRI{W+5!L6V#OoRaR&@|)G}dQc z16F%5Q`^q>yO2HW4mS~}l$ zXA;J7B2lX@w~9>mqz@$|727%Y6T|G9Qra)%gIRr9s1p5yh)#lYBdmY!#zNucf%zS&WiFK9JG z!7(>G35!fN7aAR!XxX>4hgH}UCu233&|0!}6loc0_pi+tA!jQ0YTZ0-7!nH0^nt1> ziB9&G0GgwOblggG9vS@aR#zQ8An(5~gt&D{bj{Y>84rjSOw{6AGwFbPFy%)Y6PL|p zWy@C^cm&1URjB;l9^3V_h&l?Tu(FH-2R>iOLc~~)wQvv()Y%o&*x0oP*@ECG1zx_7pT5WtGx!l z$9`{|!$cfM<7ULmSXsU7G|#ynyZ!X_ww9)*@!H3{Mkrdix3sgx^?{?+RbX+^O^e}+ zA@7&o?1zhqT#*5mnncS~{xAf%y--Pdd!SvSTP4u%(m2qCiym^>uFp)fI%jdqKy{0~ z=VvGD1>vp*#-67?24vk`+vHkLSi#wJ!^G>3-&GPVethQ1um})yd=q6mS&%+x@mYCB z;LlBDa7T|C6=B-E=5Eaj&-psGwqBfUTDK^;<+j||g5Arv;E2!SncD?*EF7aiN2|ga zu~Z00EMjC!D_We9GG>OYCQHNb~vtS=_1U1@RmLk zA+d`6kSN|aV+q{KDcFxE)OFXI1~%k$0o}~Zed~7){^LKFFt_|Gbu3iEmI3TZ?rD}{ z_lWZG)_{-cvspPd&|;!DHU#CdBd~hHEv4>3LA#EI3YblL_h+ ztj)XoV$ldLxApK*@<{^!GkH}4RgcVz zh$Bv)Q&*SXan{W@mMvn+0Cz-(zR{jUOOZNr0k`w6&!#s_Y*T39C?}D-PS%oL95Evz z=}8d#_sR;WrPyrq{#@858K`k;|29x5S9?n%vDA>u^=YlWWm&w?LH;Il94Yy5w6i`) z_?tt1Oj+PE6t5Ntcltf(R^=(p7CWGi5PQx(DHCEDTHsP$yBt}qmwe}oMq56$+TZJW z4c~UuHzZ%s8UJ31@Wn1(nobR&i$)Hs#qPlte0@rP!FGwuY-&%(Rr%B1FYtR9Hz*nd zSgYgn_+y;#nT6hOdA>W@+S=Ohu5dnMh^$BdhKwOZfur>aUtaWGsf^qGCLuby!DNTTOc7N|!z>sy~kF=2GtioMZ%y@XnUyV4wK`(Xr@0IRH4gcutL!Y=--3L%x|8L{b^*&aY3Jcc@6`}PI|kK(cxH0)p~hqJ zrLqfI-+y?(?u7z_Te-uh!b$&xm~kgm2`=lffs2{q9!LQZhhZ5_l!o>Rb#n46AyU{( z)nKt1s~IVK1z<^VqV)v>UP5{){VIyhyvijp1a`SRa%3(_!m6fxlS}HInT}MGtKW%Zk)*(Zv}`)bYqF?Yoph%S zqf-XkP*@*Myv^EDOxneRZ7ga@9$%?m8rAwdN*u{I37A27!c2}ynArMcPC`msmnxPB z7Z-+0mp6Gu2z#b&yX?mMwU zcw6(w(m5}%e$|f@(&#WYI2YqZY_)p6G+Vozrd!7~=6en^$uyy1z)@jw3bat zU0oIUq%c(N>k?)ZJ2wL}*^vTG@q5UyfFa4jEC4#s%o9`s@042=z6@9bx7%-rgu)8k{4Xl`K7WiS!Wcb&{-_Ek-P=20iV@ajrXG| zq06ON+pR{wnIMdryW>Z{Nb*!gcGFttr5{4ym6Iv0?Af#;Psu#A348IfTyHATI0R;f zNLB~$C*^R$zvPTdbL8+`PK0py#L{llmPXx+Nj>=`fh6^buq%xZRK2;Mp+$-yF+DggxJ!kBN-hStoJ!8R)lnH@RV$6B9eZ`I2Kj2bl=cF(=jV6; z;jqd(Mu0y{C>hCh%(?p0o|^NJusBKNRd2+7NR}SUTJCusDJT5!VnNZL9&0^@A@PHICNV6Z__!t+Y!0G8q&!z1!MBT^pLK|D` zzG8~;Px0IaT+ZbO_DN9a5&P=(E9V#T>nEyXR{!9Gh-2YFOVFg{^JrXe?#A7}eczGW z=R+_fdmNYL0@n1KN-{w^L0;8rsJ`#cx~cTeCx{P7N_|--l#4M^ALT=lMn*xw=9aIL zc`13x^_YQn_u?^JHXMU%2&sGukeb{pkuU5P);rp9$kS&ilst>t_cox?8T5Jfla>;WoX$J@( z^&x!?cKokM-I9fuTy7c&tGI1V6ixL^KU&H-pNDBOQ{pbUd}Y_JqbzB$Z0B51H{J9k zNk2>IVV zyOW(T^xAqp&P>zRmBOVKk~_{QP%0e49xLxoH=v_ zFwXlCC=!KwX`rhi8v^~1*F>6|%}bHZ-F|a)1GH-`h1$V_ZKBKzyh9_wyy$9x)RtH# zh?a$M^so~?)S@G_rAC`@=QP|XvuwUWSuL~*Yl;~5%9*oFc=`|%Rv#w+lJALElNfq* zUSr4?@|w@8z1=a#wX{>xEG20^$BZ^r2>XZXE?h|V{A7Dn$bfQr{gp)`4`EeX+*he0 zAIUYk-okAa*Gt{?iC^C(25E1H3=IuM zN4!hESZC*VzcgxBA2GC2H`KALQo07?gK#c#Y}VC@?ku)X~8%3oF6xd(zBg0!Jk|ZkVu+{g8^MQ^v|U{aLqjv* z2|g>_F=+1lMmv|IDyehcQarb0J6}?Fb{hL7QT2O5nUm}4`uf7VM2Ts8g6-m^{-rzT zXcpa=LKdr4=JUIYXx%D1C)Cfo)pRSpu(!T7d|oNcYs%|cd0w7X&8Mjid5a;Psq+@q z??oldR-TgI(zP5Gy8$rXeky<~=CAC_k@_u0mR{%!bQ1Rdo3sne!W20KaXG{gQl9!RM|^O#gGFeu^9n9lBQ`|k2_%bbUi8&2juCj{g1 zK*giBHZxM`R$Q;V?i$zY>i73d_iE`nLlg%FAbKdlk^kS!4}sUw=B<-OS;;1em#i2C z!3OF^0p;s4k7-&DK;f7x1b`7{1(21z7#>mcQPh@vzW9u(<@38c6&f42L^~Z`o+I;q zJ4ov+enRdeUZ$~TDY-)l1*pYixvv)~Q}F6v^T=$@ud31qNSMOs`Y45l$GY4Rod=ok z@6A`|I7}}*5V!Koox5%H<$-u#zm>Ik$j0R_ONFFvfVQXZmK)(yyAgbJYg%8Q?_YYc zo!(%paj0Zb|AnP^`|RoIH6qPC_N@`Q2>-*!f)zM?26MS3$!;F`?^lMN&80)MsulOT z>HeHgois$tAC|l`DkOL;^~|9>@|`=C52EbGz;^q5Pd2L@G(0K%eOR@n(-Geu{1z}_fj(RQgvGO2p+g>aL*<6;ofBJ0=JXHFvORWo)Ag2il2@cI zwRKApU*dhS`DoOP>uK-yhw`yuhdiN=ZuFkDb}(!4j;O=*t8nexX`407QW*PN`^BV8 zB;LYt@AX}uMG=pQ>Z#R@zWEHL?JdWtPTCCnzRBOT^yb0gC-*b3R)}|}T`z@0?@VuD>lgq)Zo>{r zm$Ig=^ zqa-?}+(+HZ`^t+K#|FkiJw*{fdC%$OKau&lVX!*hLqGLQqYumLo-c%HQu zu9q}IzBp#i2dlo#XB03>oJ{q09+uMuyKaT!NdHy-7CcXHBL{e z1ns@A1*ToSve3^;&pk+F#2OQPaM5Sct8!_idTv@nXZUA%E;AnZg;+Kya=Qg(wmF$o zH1sR$!S;u56t5H%2&AhO-uQSz9AgH>b>p)+ep660DN-Lr7d-Ogk#C7nBGbc~*3z5b z{RRj%QUoh@Q|jOLH!=IylaY3WU7lU~g6dp! zPw05zbm67Jt-Qqyr@xPV4XI3TNl{%QN86aFkSo;dPEAcIMp5uwKjq5Q$mg?y&&kQj z&nx+@>U#z#+xMTZT3F*bnqgy}`|{mWn-ZficFXQ^4`~YFT($0h02uvgR|CHSB6MCRK=m1 zp<+CXDmskoLcfV~Idn#YY4cX*vh&_UBg|eO){nB6L!FD@kHDqT!$x!BV3Ci=&?xx? zStAtRm99is-*`}v|1mTY_WX#cA4aSkxyfjfyr*Xl+J2eD{=071@Tmq1%kPxB#yi_w zxjHx5OY$TjhPwA-!@;-x@#K&k*>2lO$!ta(ug~SrUhI2$SqZB96Ud!7cs<0*fVCEi zS@2ULi^mn&zjjXODyeM2zSGzLT2L=oP)6L@b<1$SMV!=EueiU0Jb}16KR^HM*ZGkn z#kr+pK0}FQ6ve|XeFljA+?A>0)BE0^=eE7WFoJC3v=$mDwAA?b=~ye)wKA*TbV`cT zdEfibr`kV67J0z`CrL_tLJY?q_Nhn_x<%6V_N5jgN)=yzNsu@ z-6x4C!JiN$PTi~EX}5Zjc;nyqM3{Meiui~xhp^fI@wWc*2tgo*rtfEx9}nF6M*$zAFeWw+QHJ}%M4(yGH1-cnz+b&OYUq62Geh46W_SN|1fj8X`1>oKy58(uI;K_W6USKfh^BcnN*12yA zmA;}YQ&>OTtteRbd3rtJ_FFtaFU;Y!7?i;xp2FSv>Bz~;D?-vP{!QY44p;q91c$Jt z^2z@>SbuY*E5jY#^99O!cNRLv_wY&YuS`x)pLXSL%*3RJn^w>^8SQRe-TSi4<5w9~ zq4O?y*AsSmDON(?&G9|wi&Iwr^OEjf@^4sGKelN7PuEL^!3R>G3Cl)uUAYuCxfq}r zQ>BMr#jbu3Y;Oim=*^xL$hn5|`5B1hQ9i_J{dx`q-;T|iO2ZJ%^p~)!`+x2*4QkuH zOkOrPnx>=&+SvaxA}n~0825V^sav@H`0Xq1@`#IVd^xH%ffpgQm5HeWmbk!;52yBC zkY${bQB^@9$p74BIt&G$q0O2ZSQgS&{^yU)h&-ekL-zKW^*{ge&+$6LyQLK}vbW;( zWLb=k3KOTzxKh6kb{q9jx`GtIu-_^Jf(k<>oZ3R7?#RR~=!ANa%DpCLR{TF+jE(?< zrd4(9*TVnjmHCtG8lt|cdRJY&Oox(UGxn0nmt@iIb@5s$gRyctNM%%7|F!%BxBVaIK8svY+88TlX7s|5Wp|0Rn8%N)y&nK!5vE%s*to+Ybcs%! zhMUPYel>!F3#H=rRLZ-e(;Uhmtu203-_BT<U zYx{5;An=?0`&K&QPEIc}H_`ML6%-5>Z$#A?P^$)1;9|H^iT1lB|FEizD49i}5{1h| z%4Z<>ncDYabSz28)2eoVZz~o9e?bUXBIravqd<<5Wo}YPdF+O7s+}8+IzjVa*F>4x zU#$363#(G`!_dUasl~ic(0mh#Y7H*ZxW1!}-nTfJ3AEu)oBL`%Za(53(&Vitv}b%m z01W)lRaR5I>UZ(uss~1RM;WR_LiQkD)(}W(UQ<5Lwtn{N)raO>quLr1FDfKbF7&^y zZ>EN}6=H%jhzZ7ihP7#@F6WF!9X&~n_>=v>_$Lu-VI*?&W1@}geI%Xu=~83(44ZY| zaofHN2Zzs4h;|K$^BrHLx0oHYg`R?)hx3+z)fH(Y-c$XG$K~X-{#I;nSnJrmZ%>O% zE8Xyt!8S3gdU%w-;iF&#rd*Y&zEd5}F=Yr&;gC|G6YG{nwx*{>Q&8b*Xg&8Ewe%Sk z8Zh}pI1Zsm|2rjXVNEG`SQUvVPAP#7F;{ zBYu{W!jSW$7MiW;)yxk>-@-rm4=(-BKN2~ko(;L(TOW>#c^uO9wjd@YB57rxq~RD3 z-e@>!S0-X~!_U}DIn#Q%WHSc(;8%3?7i>kZm)LLu25tJNFA~q6fn+(1s9}^X8we&= z4b?t7p6S&vJdgTcIm~|^;;wq#M$)S<4YzbVEE5lY;|7ro_H823H5B-w{;>Z0)WTWO z-GH>-|Mj^(0Zit7N)3c|JdzklZ>3TNao`&u&b+Vq9*l=94feTKguyc|j7J@Zph=;Oza zhs%3a(lU1oEGR5wNu(hy#xhSxxz524o+QDkJ&sw}4A5?Cfw6t}=UlRR|Jvri-p%oT z9j{K>wbaM!4DpZd|1HyGkTR`VULaf<1>lD;$(I%E#K!HkM=-0lC$p9RF`p4H43)n9 zVfiNj|H8=2Fzldh_;tmCH~^gyI64dpjekexzh)!DON5O{9Y4uR`8AP$A6kuply@nh zjwJIQSb;iA9FYaw-=oqazrTP~;w+NvUPS+zAhrO)_t6dK;}|5s6nO)7uy?BeF^j*y z6W0Mmf?FZA2|fhU~=QvG0!#RBpN9rkbB(i&~Ux77ALCm{6`*$0tYT<(1>?A>V21F)SxCfx&Lnn+WR#Ol^bR9{K`FpUq~7t-W`;R?(k-4dv&VLU1APyZlP<456n-eg7R9h zU?Iqb{X1|GoYOu#0rkzIIg$~{nH>}T^kSmaDN2?{O*Ps>CjJUQigkJt! zA`MuQ1?DXzW`oeKzxDq;&Sr9yyL`)|*c%CdPS#%;@*SqwND?9>4fZk-8$5dsnCa#o zc$g&5InMO(lHN);`@Ni^nFhG~cF3K#gy!|Rtffym@&ESaf6ax#4R@TB)$MCs)c##i zLls{l#Yeqi)P%WMW@k6v7Ddepz>@=&;EbDw^Bxrq#`~N^Oh*;L8EiDTX9we&HZq-? z8AB%La_Y+dX*SUJ+A3NfS{*&g=H>AUA@tE}y0M$*UbPsgbLe$(i>T!Sd9 zVs{BKlH;8FFMWsvyiT2pg|ZheG;^pqqB-St%N?I#HnGIad&a9;mnTOC1((kE%g{8} zzW)MfjsI2jXvw9RPV;(fe9>8zSH`BLt?9&{F?Sju2;|^Qz4UDV*Yx9MN|Oz-zn4lC zlEgjuG-xD%7`NN2NAO}IH~@7&S_%~DQyb7uqyBBus?w9=)h0I2-86Z^KLOG$Mpdzr zuSj7qdU{P!^v8IB)0U6%^8PQ7$GZd7Oo3^`@qsc2vnfoat8Hhy^Yl$n;yBOssE9ew z5IG4BmD*EI165YZLchDqmj7R=&|y^+dxyk7t~kJBHJDN}VR!1`_FS zQSrR+74JPRiJGUe8=~#IQ@Z$I$ive}G~}_ zFpT&V+(aXvI!#j7=f?6qjpk(mhuui5v>5z#vf!dYdf?kOgUYGxVvE(C3W3?4S9W(E zVW{^5xPJz5b2j;wx>fIUtG^tNyBbiiE>DYED&54#tTpvryUQaFnBZAnFL>pXl}^)b zdpD2Gcz2m@ex7&W!-A`=tZue%`4f~d>upwT&Fm{(GX7!G0W-g*P{_!?p;nUvUye`6~!m$B(5Djaf$(U z6)GiY(H0SX>IzL%d>J`;vE*^Ql_JjHxTmE)j#WPi5|X5tn8h8nrJ-l3J#81lqaA+; z?faDGvp82;1SD{TlEqN>j$>fDt+jKOox%6nmkMDNR6l`VQ-$PqPw+&gi^NZu{!AU= zjDKHP$nogC$k+B{du=1A8vk>`-17HoyGg3n3X0R@{VU3w(9cF_+({9aUXxRz@}YM-BY0b;$v?9tXsonuT9L%`Ea~Fb)`?Qaq#fu zgo|4(6(;84r&SO=3)5OE6i-r4>griMIvgV9K^wLh(_Kh=RYR&m*l+ul1G&@ezv_-=T zTM>9=UKB%1n#V{5+nc(JR*;bv=nByPh+nYiam9VZSdJu4=tt1hKj zu|XE0!8Ul<;MIz%+on1t@klDbJzaMei~G2~P{V5Zav`7>A{8DlX}<5T;vKm2$aH=<;#*k3#4@utb@<)#42w=@x8;R#Xt?aF@sX-6JTs_tBQ6{jaAI&ThGU&){%= z>bt&G`)!Tr;QGHq;F<*5t2Y?^LEWZ(25KMIVL7rPiyr{M#08N7-_J2ZMl6s6ltlb> zCH7;Z`TSZM#;}}k_1E|IR_H5JP^7%8Ncpm+)|xFEbjaVFsAHRR)a&dgSDhFwgKMDH znOOnGPDq#t@Mw4|X401Zu_a=jlP^N+fO5y(F12l=mGy17ybxNLJa>@Q%G^D3ptHIr zk%^KkE#*`)&b0JS&dbciO&)Pz|LFb6z375bNSsnLFN%x6xyo6`?bEjiSE@^eggjr= zo$!ftoM$NLvVO=*UZn-1E-|4{CdUSF;g8dAxlIF|nb?OuLh%QPVIIM*sut+_@qlBA zxDx)?z8J<&qp+HH1j(KE8;gIbn*s_lzB5iqET5!eVqyZTpKABgLo0f8!VUx^9iW|9R;aR@)bs%TA!T^Ac=kc?thH#l;Mxf z!}#RsnH&wo&qW<1vzJ*12Y4P}I^&)!Qgq*>8u}!(qo>qOn@6oaEG6zsdfxJ!IP_xr zzPT)=X9+3YCT<@fw$XO^w6$<&ohfaCP_oIftkv4v=Om~pqTU1?3n6?FG1qAeEFJ-$ zU+x+j*D55(V=a?75n)_{bC*h+#d$A+kpg0z2o7RpMC0*~6dCzr2BP3CGJYNf+_xZ( ztqTS&Z+xgniPO{)#e{z`=Q&k>%8}k82EK4Ldd=lCK_`XCuthX&drx;u3HDO*d$x?n zbuhJ)WW#S2c4a&-VWjWN%{vcNk3yjLRJDn*xK)^471o|!Uy!;bG5DGLu60dgVtST{ zK;jFH7@$%$#V#qT4)yWit{11@C0mbl?7npFS&<}Hh+oa~+{&ej<&B$m)Lguo?{7tV z(n?10&qRf34^Fl8P|vS4&X)X`xEL;A5@)M!3Y$4@dHb_-3|XPBrmC@AAMH`N6Ead{)%|u8urSbObWFPV&Xl>xA`^v8 zFrlN{Xk3=~I>6A+z(j1HA5z8h8ziYS)u7CO5kk1`BL}9kLDbP60`V_6;2W8Lk?zbITdcZ%ae8(5RNuA>BB+)wyk8qttJZbQpEwc!1r4s3R5C8c zaN_fFjAXes5I;dJ67q#@g8$ z&ZN;AG6r2^u0yu`S0$iH-)ROTQ@RQ*+n?s88!wUM*Pb&!gEvC076Fm0g$L%3`8fUt z?)-~8>%CW#F2sQX>emEiEv}bx=RB~pX z^4=Q2@l0excj^M|YF*#ru}@A0kUn;5Jj*SSww(39XrOtp-z+bapm&N6ZbrCY^(p=9 z*RR{^ZFF}iC@7?9;14a;ySzaNj8D-vogwsr95)CroAKxtyvw{MDMb{05#$AQSd~D~ zw;wKtPP}~SH4h+?Pp!7{DFsFkzf39NX1NW#+LrmeT3%SQRs`CJekU|nX7@LdlKi&9 zv-gNHgX>@|(4>a%*5yafKxxMYbTcFO)5P)|I%!O#s3f#=Sm_f(Pn8Gfzn)??0?rN3 zYe)WZVpdAaME(PE;K4Bg2@9_K{D+Rb45IvGn+FOY4_EKe`2o4_{og`XUGMfluU(?% zK&2Q!dD;$*yK4!M@RP8e_1zcqu=Sk2X$G{z^e(z`wRbMZuW>l&Vp<@VuEiF)3 z4XczBH?gYPeGp6s9pzsKh5XU~6#_w@d45lwO%+svPMa^m$>``kdZH8q5t6Ep*11D= z55r$zjLfFP08(Q&i~=A`z$W}19>5MNb``VFGs#b!I?KQ<6D#VFh7j{Htx<-l#2mBN!9mxu@2?}`KzLQ zh|++-TwNed{d(KnoUP?r;f?#4hj!;=04qm?;^1PbFhE^gpG2}w@LO~^s%Tmg22DwS zqn){{0?>YMX>X6VD5|ERI`&GpG0->9UR|*n{Dd6ipMm*{t z*0?Jw;27cK*Je$ghPOc@;PLz+u74J8=CNSte|b@K@(Ah#-eo7#kYJGmvB27PCq%i^ z7!8-9(~A0$&+z6>Hf&(id6J~s`M)-E3XwQ zx8AB%?#xuzBu)iP_T$)?Bk3t&2L=CMNYvjL0XxGz%*imgnK$3jk`SkXYNE*?D8J5T zyw1;0mfkLPZF0wmv)j}`lpI~ z5U5g=5t&zJsEiP!Dsl%QAJ#!-F=+G6ysxXnyNnQD!T@O z)zz{8Ky3f~u98sHWrPUkm}6y_+noRzUz|UftdK=BtEgKlxCdRjD*U?QsMP^fHj6hF3zNimiL2!GoSn`|9we zQ6b|9r)=r?Y*$*+ukBU3)i>X~T3?iIJ~SdQzc|A1k>6}DF!?XA+|hNb&CxDRbhihnj{UamjQb>r)MH z{6Zv!!dy=-YvP5wF~2&lNEY`9A3tc1`-hYKSdPDMLda2hp@Q^L(!y}D-e_zs2zKIX zpIH<=6e2k2mZ?Izs)BcgYaJhH&4sBh_yBpUL0!ORWKJph6dPQoQS=&~np^%6>ZK~} z2HS5eFDcSqT`%S`<=9hVGc$^ypl#?TFPm3{l|uv`!fYKd{#GN8qvF|N9Fz`mNBby< zR2b8gA#u&HY)|^54SmU);#&xi=S$-IcYvfzY$dBE<3sXJ*FtUY9!KV&RnNEv9pPB| zZO=JGwHi3D&iMD4iSVAD36`Z{G<$D_+C$%sE;Fh^ki5Tz9Kk~wXt>`;OXFf}9Kt)_ zu$&t6-b}j%jY*` zUj`=>R;@ril-kf15xtju%?N?cB|QuJ{w*-Z7wPzw;mMPxt$iCCGYA(2(^mv#dJMQq zhWnB!oPR44qQM7ETiyn-8C#`dWe0i7I3eKAzxWB)m=w@95gZ$jXRxG_M<^c@jp6X#*PY?Tjys|0>T>sRO3S=$j~)7_-y|uCzX%D z7krtbnpnfAdF|TGHCDq2j>ZPYbdUqNl0Z}%XM(W*D_>Y6E4koWrE5-@AC|{YP=G&+ z1yR>B`@&KGX(UOawIwr zn*Uk&X2ZDtY{q=JL-;)#WMpLh4X?p<+NLLt!&9BJ3Gc_f z1LM?)j~qL3iYQ7EY%`J$g$R}8FC^btRY5+9Cd$z87$hkKG?1j#qd~svE4>Q{l)&(r zxC2a*;R$XaO`1F!{>K0spS=enLRuWC^EoHpGiqvUPs-O@dif7FpP+(1N#R4TMOX)@ z-FR=$uPo442m))e#=Ek!TpJ&QjOZ!q1K$Ch;`?=ANw#D?JP$lK7OeK#?6b@%L&Sr3 zM*(?d^rlUD_KInIU3UnA_bckdi9x{D00_PX*cu<(o8KKAC!)JHwr@@E{c@7ECGsV~ zpC=r@3{QARjNkdpd9+PCy4RLe>k-j2NP$~s$%d?O!FqNw^ywO_E*# z+s>#&6m8JRw$Ai%*<-YOn6S(i9wLmm>AbNpfP}OVzny{XvH0(+s|hx35Rs39K!Z$A zKJiU%r_h15pO=9VQqhO&Z(t8>E)YEMXAxW>fAixQ_bkDV(&GUaNx&IvCH^?0@{zh1 z=wllyD`pgWrFO!DO?D^;`RGB4UQQ&Md6wKCB}K9T`x|4l_=KwrDw3EsC4n*akLd6QPpv7ZuLZg zWB464OlPdC8=I(YMeo?m_{f2Y$j|Sb|F2(nnA0oz5-_4F!8_WnS;@^MlAN$ANJ53V zCDD4VMPIMT8HAQo1cwxl<0O+TlLYb@R`tKAex?U1SKra0A+s9=_C+WC_1SL&k>M0B z7Z=w*i7gq?eovIhoH<^ysE_PF*DmC86UpT%?3@fRS?6&2m2D+QWa|p=C6fZK!r*P`qPFK3yAns91s3gWkEzC70Pq=7pSzl zhVkQCkD^MRBBa366_@B4BT3On3Ko9ZD6iz0Z2F=9_$p;4xNItcDms52%by#W)Yi%t zzSO9c0>yOWk;>|tpB1X$o@YMM9is)gCkqCgYM5WOvF^yNw(JLS&j-RxAcXcyPo4wk z`usw3`TU}-2N@(636&>LVAMP&tsnLFQ|8}}(=}Z89lGSXu{qOO zN{a)BrYwngdrLKZDwyXW|1$SXXCRVPyj;>+g*i?{I77gAKdy3&Wt(tVd)MJs1TxZT z_SB7(!5`p;!dI4)Zb2F^qxk0%`T`=0Tte0k7$$pI9&mM}w@K8@?GN(+(htXlr`Fox_ojh?e&(hM~dV5$@!J!~m1NS)2`ef>6m7U5nChq(51GIfY zo2f$`GoQ|t^w*oL{+v!0?_Ac+LuS!MeYGf?@6K^vE1s9q9-pufpKU7#+KSfo>()Y1 zc8sCb_k%jAPH3QXt!QqV~VvSRZ0m|GP5|j!%fZdJx zXS^h{WmHE7=Xc6s_T;iI<+iqV>NJ8V(Aj;)GLv91_nb0A2&$(Ow?`kado$>0XL?s7 z%vX-6tyf@QSCEla1Ch*XorT_+xQg9t3p88kmyPQS53SK9fW7h=3O@ zE`h!a|KStsAoW@(_xe__A1_y>FHPT(wwrY7JwpLkXJCN{%&j}GZanq=5z-^>{0`vK z+Yl*(GNfUYFWPp_O_iIQ;z8lp{DHzHBEE#GPdkX?&F=F z34TNkjBlt=zgdVbmCd)0c4ey(-In4L=oGO9Y$yt?y|zHGwW&%JQ2wsiGH`dAo)ufa zpKBv<(CynpTLKeU3S@ZevpY?^KXoH+2)OUEMcd!ITwr?EndG2%R0h%N9zJCdG%I*B z=I&;a4r|;!Z`Gztsa!1wn&s+f6?~9q-{aOV)9i6-gy|uCKwfwPgz~GN9GEF^?uL; zaXttpyPoHS6KOJribL#JS;eMFX8@Vz_YxP@5)Wr&;%h5kZ469GZr+;`w~Hj1Ggn08 zUQCcAerMpDX%f?EyBh^9(dmx9sK!L6B2Sk$T0xRa2+N|B>hf@Pb!1zFpps5QVrXK) zgOBIM@!YHFEvJ=dgwX}@em*w_aoP<; zdXf23*lNI8vc?0TQHBN#?;TE9+JK5oY5eVwEFb6Zo^yS!W(&Dt?l?oi zW+o;kKVC5r?wIJdpQIMTrKj$jPq>v>csXM5dPK>?g|im#$*9gdnp+ePegbiqfz=CU z#|HAN%>c3*4rs%Kj`|W$iLMJl0&S_A_voy}9VBm^Z~9ag%GC3z&}?3}dIm{0AKA`s ze-^uZ_+q~d%M09c)-x;?b=iPX&psK)vLUo&q*{QBZj|KoQ;>0)_C+3nwC)E~1(mCHxbtj~@B^ ziS|RWE(j)7Dbz>|UQyu;%?H@Eknm(Gp~N$9`bAzxL1q0A#-{{U$0N5I2A44cMUnTx zi!(EC%R^OqhOoaiv%|h{F*y*BN6&PEqG=gaX50B!qny|I^ae4(OXC!0%DOL8UHu#= zJ~RBBtN*Qt9`@k}*Y#?(BkG=L&KXHWxo4)l;caei3g>IZ3OQ9ySz*a*wL7DhJltXg z{wIhuQ>(-?Z=c6P2U0dcr}2R8yYI6Pp$1gp4NBT*b{@p~RiD-G?KUz5vF7H;xvwQ0 z*M$8+o*7)K{1ebR3m_(#yt(IjZiVEKj=|-H@;PFq2G$Dqyd)K*>9wJnCKnBNLj^i) z@}+i+FbQ|Zv-REEn2{S?$?r>oIOe7Ti0ESRR@b%&!Kp;}0>ba#7aX5C_pWCNG@7?h8%T|u17R5*G+Ecn!(rB}|90ZmdwBJG`YeaTg-oHy{WzvIf0k-E@=(!X)cFkK|- z-6Bj$(2ZHaIYz^POe%wt{@luY0rzJ+9?9=*R|G`W;({T41Euo-NF6^t2zqb;eH?gF zFqueNaHi%>5}mEoL|7#l&0~D4(B>m4UTs;~HDv4ufb53X+$YwHoF{d3Ud*6sJcVKS zVS45dB}!9ds?ey5CwsH(Zzyt{sN9$lo9G_AVFJnpk1OM`^7$W&mNMh?oX%i_7>}?px zJaDbzf{5)9Bddj1o8J5X$JASfRn>*t-_j-B(jc(uMmjc|mhKkmE+wVATe?9~T0mM5 zDM3m)rKGz-{*&jN^Iq>4zv|^)Yt4I(dyL<3ADTYRsi%KuztE{T)O3_`I;JNj>BqJ+ zR$QGuTa;n={xu2{^T6<|fHevDwIvu(S&faD*pDLX^1M@C zG6CrnD88o56lV1%{pBB+7yQJTSXp29;~@MFOT$5O!U)WKi=4cP*d79qUR5jM0^aA= zUf^Vt4sBEhkVJV;yDqRO5tO$PUoigCh%n>>;1_FjSum!{ZC_^Te49MTix7!tr~r$Q zALVzIC`TXl@Q)y1$Nc}B5+tZu(8`XBAJg?r3*>1LX0_`Si9(4?FGyaQef>!$DuHf0 zqfpFT{iF>ifGvc-8f$ZBJ8U{^S=Mf5UiP1}g3ebkDOHgmKvSyH8HDuk;hVyP>g(&v ze$QdBw6eXf4wFx27zBawmwEpGyk;#v(2{E+P66?22MCT&+k4juc}98QZpJH*va1J_ zHrODn`G6`*<12|}bp6vi5lLh9K8s(9;yL&ma@B>XIG?mVd(YU4nWiTg$h|0ozQ5hf z=zbn_JI5n;5kL~(`a23XCm5s@_E`68MCCpZ?Q<2YBknfWO;VwnBf#AWS3G2ngmHbJ zkX4C?)EUZ={@)c5T<}uxTj(1jDwdU#aNR>hDyIe&t5KypYCm)}y~c9XNr_=c{)8p& zcGKjU#apFgkU8@U`}q-d9205sx(@q}r>`|xnrK5l-g+Fc8cyqc9N^*68LHHw+Vwet zWISAa!6U;3AUO#UASR*+inCUf#>MD0h|%AJlW+*o>FE^H*kgTYbcx--&J*AL{ZWK+ zCclF%e-NQ3@YnzFQz}6SV1p=bzSRZM2dQh)-SKcv;`=6Veb9UzEc8V6iKcS-uh9{JA zh{(l>x9DmAlEvvyi9tJINqg5RrT|??^Fq47dVfAU$Gv{lQ6g#dD5M314nCh^q$qHX z*>jM$q1ZiATbb;!lZCXfPsC6N^Y8!j1q!zdro8`8;LSGkw95fwN@(6hY$ZH8O48Fa z^_}`)#)Vn*X$`tWQusjEl^D4u58MQdL;&PR1`qDSaeozgvFUJ;CR2{54V(te;zb!u z{BBBS-@@83m?g&9;C>rKQ9%KD35Jaca=;3@DXw6GvpR1}f0)}tX*vf-*{ovszql7u zGxi%lL}(=^pHRt~o%b%$RST^~{2vcdsj{TRU+=2nd8=0!+spoyUpG>J5tXLBp90oJ zu)K7QtC3jfUBuPyl&Z|X;?K|LYfhHQLZ0%|fzR-D$@PnE#`EKj@`$jG2{?ICHSr(= zSGP^t85C~j=jBTqI>AW|^nGDt9u-B08EoF}Ju}12L!)C?r6O7a8`j4>X|knk$y_wJ zQTUo7=Xo5{mS1q_Bg|a4N*S!WT;;F!iX;d2R2pl&-9pLFw&zZlV>?QWb2hvqqoys} zcgB6FoRm_+160sv-zHUnZxPOZN8`c0{g?h2QaS+YYGpO)6xpIFv&wk5Tu?#AB8Qab zxM%KzTLhPh;m_52F(!y2TcgVMalo-4>SMJ zI6@=FQXcJj&mWT~2JMbVMBn@V?|Ua*p@Vh`hcuFxdV+~!*rhdt_|YVDJE0=?kJ2C` zgOEQ{m2bB~D;0W;)ro4B8N{nOFq~MX+9f+19So^KW=u2e(w_Z>az#8xsQzN#iv984 zx`YPmLQ+X$VbI5ZbEIQuvGpnodZxt=vPf$;HcMp^Sk^`ms|yEuBmF5i_LC982r~s_ z!!Fms?|Vgle??WBpKBy$>*zKW7*_i_7R0ozO4RJfl+wTkuL;HiHSwQv44Gq8iA6|v zC|`j94P(iCzw<;GY(Jk+TyyZ)cab4`4aVGF4ALittV~{g?6mmAgDjA03wsjYvFp!Y zc?bcIjX~gzQ`r3sok-!elk#8Or=@~%Q7eVXryjrt*@$G!O#nB7$Ok{CD|w{8Cv)IE zfY=e--Q^oiJGlczN*Wt84kF!rbaJY?es0u!G|onJA$N5tdTCgE+Au@vB=qZvO8+z% zfExy}V-qlAoGz^Vd&0kIN`_>PX5_P>!Wi?T2_IVdlhCL>SFje_JV@C{qP8YvmZ8!c ztzYeR9_+mEZ)(K-c+wo#byrphO)u+g5U;0WWEVwmM! z=UVVFzo`3i;cp*x>I?=|rE=*_l_%8bJaKHG&bZMt&J`U))|_ZHks0Q9Ux=QuMR5^ILts z=Q#g2-yHo&nK`srGhkdS7YyivWf<-rWeO30*h^19dS^~v&th*{__pyFpmOPnR5+Pw z#~8y@L{4B|!79GNw(8w<7vecznsT5FgJ@x~TlT-4lWXrV*_3NfV20meSiN|l;o!I| zO-~v(A?#fQeyKu(V^1$@m*xWa2r$pev47o2s&($pN&D>546y(*zbTC}YhlV0yi?S( zSSsZ$hFX2<`$oSmK$cFWC13;Y!-qJ~yEQDjKtEB=cj1y ze2Z{jA~nzGTE5Nax757U&>)yi^1nZE^lfT1`{nZOX;Q5RoUf}<-hK`^hmyE9B80&C zlIj&IUEtsyM=5MzdEe=wPxQ6pHlfnS>BCp;PaLz%E9zqtwIWuVs&JJ*5vTD^xk5C5IC}vfdo< zLA2E9=q4%5LAq*^6+sXp9704SmIwv4;#mxZWQGZ!quTSP^H0Z*zalM_wv)pgfl3p`tj21f_X1LiPyY2T|MIBzTbsqTb84p)%C-FoJ)*t;r4bPK#qas+(1R6I8JA%GpSkN~JiK z3}mx~-YeKRL!m*3j z`#qwp!dcoU+it3?AY@;%gSrF|L343B8PnCHCW{IF&5=16W>+5pLs<_q2y??grd`c% z0F+Jf*-jLr0Z?v0g=RUlP7@oCUx1L2QqVO7yci{hEYzupK2}nwpRg!|#sJlWV9N4m zdI3&bJ9@!NjoN!dYQ1}#D+5%j^tw3gVLula$)shh;WSYMwhz@|4pd7^FPfLIs_r$z zsQL%vee2h=-B{QM)Bd5O`onqMko}C#nhqqfJ;{hQqV$wmPzm2KsyG;q=DB|oBWn6n^DVrSmPYq--ad5czu zHnZL@HpeCA_=A3WMwb85@64>GLepwi0V+Vbod5Fdq_B6YA<{P)>sqL1j^B?3qmZMiiUK_j;T78L&q>TRAi|stP>7Y)Z7+1~H(CjyV>27Et0{MV*)HiuF zKIWhGe!vNYs;sowzxTUxTl1oRks7so2LwK4=u;Gdvo~mi!Gatd`q@cN{c#jg-m(fo z{W^t8u)vA$K!mIcN&9y3#?mvdT(^!^6nK`W0QR?`s&f%V9leL(8!b(V2!I#khQHa! z>+nNp2Q7LZTLJn`sK@F2@3?|rNSH0i94vvTdM*aGMR_mf;Q2xG$^-!8WKHOcxIHCK zQyj*JfW1va4l>&XEcly#CixO@YVN;EZf-I|xfQ-;llpGYqQ7rq$gcc4S9M12vV)eM zGD;?!v%A%xRWbM3WkJ+KQfTpwcw92ooq4~_su_`G9iD^2iR$Y&pur>Qt>afbNS`7+-5b+P}z@&_|Xg5EECjR#^b z8T?e8);@exY$C<&;k4}u#3LguG1p#s2MM`myLxOgHKj^5zcFtF&1B8)XBoxU|9sp- zc>C5wL|e_P8E;M0a#YfLzbPn$^NS;wJj`WSZopk2?AgY^mme%7Wp}VOnkWLM!bA6q z(}$}S9dcZdA?0zf0$S1za4VHQb!+kFEi&QO;=|#NcaNUl)EAOwhIi#5W*H4PYdPXa z9(MTA7fS4G0?TtDz^19vWRt``f~nypZXYT+%B0J32QdCe!H4TBf!RJdY$pqx6q!bP zqrkBC3PzCKa4$-j^A5TRw=L@Da~ZMzU@IAhPx`U|;GMLxvMXl1)X`Cy*FugY<%+;I zB_yUKFK7N)5g;3*3)+HzB#kLTD)e5W+Iu5WwhUamQW)c}HZ!H*X?|o#9)17`g?FC? zK3vy28Zx%OB6X4JhV zD%94(-x+Z)LFEbjJpY==wcyf@g>WMHZ+f0CBsTSKbU|SZ|2N&FuCC&zh@u2l-Z$jJ z9!dh2nXInAOHmsuk|a2OSmomrSEHOeuavW}vRO&AE}<3qiCTLkGHrI8J{Y%#U*Hzt zbXYp}A^l3QXd8JjUVH2NZ+GQ6>n>f=7S=q=G!E7Wk?4#BHY_LN0$o zOecXqU(vZ3R8+z;`0Ufw4iivkLjvS*`#u%25%voAW^2OM#CH{?2^6J@hhNe%-*E9_ zYw`oELQ$5{X9Q?NAPxsL5$#k&<6r6k4+H^btC7r-N}LKl@KgLQIU!ROAA3O{djFP| zkKi2hh{$~w1U780kJk{lJ0ns)eMWc>KKNQRleFiNyk>~k$D##E>c@wM0O=K^9;Ky@ z{Z~){@@J>U;ARq1)ur>(p{xD8tZT%2r%no3dMT?x#q_I2_afTB-Eh zaPcJtad88!#)U67jz2b+zJ808qGr=(P1o|0(PYC@33aukjp@5AO8d)0gk_JbVan~j z_2J7Of%oNEc{oDM`di?@3CuBYaaq6M`r}VeC@nbd^?;=z?N#qW)6dALOX23P>|ZzU zlfj`y-Hos2iG)Ci7^1L>NyWTm4Ybc9a7CNw4n)9|hy3*b9}pXGB~VC5k(*IWMuj23 z3xlfNU4wmc zWu@y5C%nwd6i`~B1`Lp|?fOLnJhW-|b4-3R(V|D-0@t?svW4Hng^mWE!rQln(1`B4 zpTIpTl@x+EKUb2Z?wkuR9ukE{#EyOR9n2*qD3qQOG+a`HW`ODSiSX=~@#Ig+rN}EX zTuB^L&-`4t4=0YsB?^R-ltFTS{S+QJxtJ5#Qmep4jim}5%n^Pi*%zhrbHgBTy9a=V z3KYW%Up~fF4zHeKj`J}qxOu%&_dUDiW3i3+HaC2mP!LAyFuSI?cxf$=XaBd;D(ZM$ z;HFN+JzEw0JduM72Y(CIk;$W|7u#=xFOHw9s@~M;Xl*6@gxI5y4^lPg^-g3dC6 z;9-G5-xWjv5HkXD0YM+zKtdpZQ_)lAu5bTk zA<&Sdz7hc?wWvLhT)?>-hau!&AT0{{CFonx)?9kdGfeRjcPvRAB-88+#TyVMrk2F+7OCXDxz<_!$L(PJ_?fl`d34? z6<1(=!KHtZ-{m|detN?XUGx$jiC^D%8vVAw2Y&l}tCtu&Ce<-(3q}=v=6}X10tpjdj2ceLvDz8B$oe$!`mgqKf^!+AZl?s1 z{>@tl_yRd|RJ5~&t-lcRyE+xx*HTJuJnW7gV#rHIF%yzla&L;}?qUsCj~$ObejId{ zL$rqms@M(F3YS(v> zFAGEbrcabbQ&x#-hk)-(zBtYWaA8@lP65z*Je|voK(_er;uGy1ilRx0iw6&IsyaOm z*4RA_?)TRxDKt{Yo}Iv2K251`wd_+v&scg$P3%O1t0qBEXNxKe|5gQvd<00SMcitX z1C6Q_&jd+c5+tY<3kFL7s($JjxdKySf;tFJl=CJqS_)0>&Zc?qr~5GwjoRU^@%jI| zp8~||OaT{KFuW+zltwK7^0_)xQimH}c0u}D2;7@Ov}m^7Jj7QXEXWt~pG|2cAXlOX zb&h((Mn&Lt_&ARCnS!HJs&^C@bz25Z_<|rJpHoOwrC?mPsu52?J!s zmM>uJOiabnS=Ef+RE}#@Y3yNI?N}NzunF7FgWE?2s108HLhoKU;frEW&ARq-8v7#?70@^n}al?4SLm<7{{=Ht_&>>QS6=yXP;-?J*@B6?nPTZaE*aco= z;Q)C<+l_nqWL^V-3aHKGzbjeno-WXo6bQz@VuW*$Yu7y8#oG*XKz+KD(ui7Pe6P9! z%L%4kvT{V!9uFE zLaT#NN^c?Fx;nVq=_hq4bx1l#HVWMM3L!1Ghk(Vf*?nktD9b3` zD={lmFW5&xsTZw34h^)>`!$kX1*P?#Pa>}vWqdkZ0P z$WTU`bd@^1G{wF@58@x3e5IIQ5>f_f`GGthNYowXGjjpFZTR5dFU;xs-^WICF|Vx> zQV>h~zPZ`*s8l{)Q?=oTJyRrvE7(|@oDH!GPfa;npujGP%s8U-bw=U9Om9IqS++#F z4O8#Y@xZ`=E?u{Ya2-S8@pls8leNcmu(++v=_`$@hbh~tWzKY817o@k*#5OzO0glB zzJYCTfX)F0dWDpHz~%o$?sj`_(^to;t|&kFQm02|wIt10{rERgKrN=%S(F)}-I*zc zf~|fj?~ii8aQVQm;uNj`3v3|XebFC}5+7;^ibS&}o7 zhTnXVhjjJt!0Yir5*B!ik@6`Y6}ig!5W0gZzDOm>zrKS1MF7?sXNRd^rjIavoi$kT z*{DtX)u?K7x#8FP-@QgzDlj43L=D3QxVc&a#0l=5O7m}Q*q~CKOHz5H z0&z3{%_kJ4mtU$L9*~oUrJ9LwK6)gU7k+5CM0}``p>)9CY*j*~=EbJUMe*=-THbVk zmnM?hnfym(wC>61TpW~+_BI0F^4%}P5opsYC2m|`_oe#YpD8DA>bkX3%u*3amelImhg4jI}rQd)VzEuPm`=4L?K-1P% zFtb%ujS*cO29xL!3u&FGdG)w(R-Zb;=7Gf>8rHian98h03$X)T!x^6NovGZ3{c*12 z)gTi*Qmat*l8{Lqrgr-$QXmW^v<2bneJ(2^yfp-wkNfkJj1!k+)%afUD?4qWfIPZu zJx9HL;ll7XI(6Z*w2s$)bMmxNOD+pezP^N!Q_zIVg{{e{B&L_1Bh*a)dS==}qcF5@ z>o2>#gnVwChH#1hZR`J!YP0zP^<)1?%ZEBcs?mfa#WlkW@E`0TCg2g?%qCBd$1@on zlR#Ua*nq9eP`G+6=|Nn?v@b+B@`7?iQw=DoA+8%X+`K9qB)4@TF*}nua&Ow#e38w0 z+Z?VUhu_68v7c!3n;6c%B>2LLvlsS2AZM@Jd6k{n_eJs5`sof{UC>A`CI&2KJHaOk zZBPm|JHURM|7%4jd$%ZWh%iwZW@Eg|ldaoM_Z%@9 zXlohnPlhWvSc+<-p|fR=9(pQ=$PA7xGCCC!IzhhSSJ#YbrzRdIrbtdj@{MOHS~bR^ zL)n52b2LL@0(aoBwE)(!MAyY94#Y^-D{OD_`Az^Jov$@T_+4#eK*Le2XtiRu2ljjw zb_l4so}FQD!9X@)(LBMi1V=f`+3Ih`ietG8o>a{Nif^q!SH#(msJ+3e!Kx@dqMCQT z9a&xeC@eaM9@y|C1_vDrrI#U+#fwo4LZXaz$CyK(AS)P51Pl^}Gb=lua9uEn3XT;*%u zyf0s$cYM)wC;F#ImGt~bKvvKHbbH|MJTCYuqnhvIo#)U zbvC7xPlsncu0SqowOsape9&yPRx`@9MxepKS(d&Nsekct8)yo=fZLHN^uD6ZnjSMG z4n%2ivTX(3ws?P*F2Ny10Uxf3i?i+Icr{odK-RD%{%~@K*hr=el8`4@TY*RIHQHN1 z`z!^!(c%OzRsYFPN>7UlncpGVaZSg_shE|@;{X-&w+B+d+7-GfnHLiQL8V7ZO4JHQ z(DX4j5LMpdk$v{-)dkA`j)DRiU&Irn^YaVX3si$0SWl$%sQm*yiT1@}T0v785y<_O zqS{K7D5OsCO_y8MR3|~4o^s_nbg1EAeHX&cmK_UJE|x2cA~VS$JCNocKTLUNXa)*j zRS(Uy2k_lfqg6$~XsTipo=R9I7Bp~q(ocm?(7Vr#D^Ie^qG}ps`+NXOZ4o38<3qxU z;Pn^mPoiu`f{-xa54E&=zs&tcDILN!rHh0p7f`ghg0pXEk-T}STr6DuaKHo`_8L6y zSzm7jL}%(rHivU1mFnBi|C1&@qD;Lk%5u3`q)x$tIvAS8^)ZL8m&*&<`Nvn_Jnw81 zP{-GaA3EMDTmDnx2r@OJdu$V2?4JF)_a_HUI>BTv4YpdD1_mz zt-c}2IKCTt8UbtnX9R?`V1tNOLK ztMLok`UY3RM~<%qu8=FhMQ8nEpKv0Juv{%>d|15e<`Aic&cw++ zJe&nkG(O$b;wunbgL01;kfVfw-HK-Rlne^HaeF$jPROM@{309m7r;4rVpOS%6To?{ zOX=mambIssmrIC->ZAm=2ITxmhN%7nY=H7};sQE}sli0L@ISTT=+6hZsH6XclU|9; zq56iqtvFe}d)zuXGw6!FElLur{obJbU=vBj42QTKd|8?#*7zN}?7Ovp3;Ulxe^T}k z6z?T&d$8l&E{I`+HPb(cm3g{x8WQ7GR7S(V9_$o9p@`73*e*1TzH+t9>w z?C+={h?RN`pG>(x%o^x$<3!I~pZCPC-E(ob4fXA7m(@l*FNl`7@Ba#yBr2z@-+Pu1 zSZul#8`EJVT2;C6%(3Ph1N~mBcm6`6!I`YMSJ*{_Y*_H$T`jPp$us zI{$a9zkG3}I=L7#oW=srI8)$XX;MYuevNT?tHkkzojtn)3n@sIe6S_l-KqkDPeg?k z;0s1J8m1~l0&4saRMZcHFZ)zOR7)ygbUmAqXZ~|>yKzPy3qvdc0^ny7!y)jRs9p|@j=EdY!(o~>$I9K;FV=|U4qi9-x z+9O~Y-&c`XI=b89k5YoV6t(#u;@KK*f5gN2O(*R>|+%fBLo(YG| z5Gb7lrkhVM38upJZre8plfzY_tcAI_oRN{7_a{qLbT@+pf)ie=W}hTSlBbRD=jD9I zPIx7OtQJW{?Q89?KM~%F^#?`Mb)UfxhA&mribW+>6{7y_VmP_(#_U)Oy^q1LOO*wW z)Su&Ofd}`9TN}C`Q7idkz*aQ@POu0XijIC0(q(emA0z-lZwOuoReF*jI8iF?Y+n-4rKXy342q2Du?|PC$-(#Y= zAbjry6K{6D@{R?5V&A=7{`o0M=)~oIMxWbI(x`we%*Af;Qq|dw_6Ue*bc;6D*aG-7I zp4alT=PzRJj6j;3Wn@gtN}aOUi_1wrh0bCRb&eGuc5_q|POX$kMJ$w`O(?kb`-{ct zinHO1G-*`SgldLj5?r>%@(4(YT~4zqh@7tPJU-Vvh7qC>$V5TXYE~kOb!Fqy-EG|HI}neXa@QH!;C|hlB!8+9AU52#DXl#%#A8*1P|%!@S2xRhPh|M48z_PyKLp zFnB%_`zX6J`6CRO`QIL2eg?NB?WzOfF`SVdEiL+y{hAg*GS8 z^yq4T>lP*)fT;#_yD*PS83}#9E@(1fk9@jx&>Kzzr7*ZBF(Dz}aE7`XiD!B6pxLTW zON${yqEfRw392t4*94D*4(U=Id`*o&TSevsYQxkcLRu`}@Yzn~1J{5$u&Ta;Wq|%p z^hk(%TC|Ut6S#L_P!~{RqcWqe1^Yb}hc>?3Q4jt8mha^YlXz=klg3=4dbF5mKe|Lf_ibbnD= z9epmT$0ty2bdQuDr4sdFD;^_89UNYS#oa$( zUwG@`A{7`k60tVM@jF5GP`543oiAyokS&)gk^c}_zV8BZZfotU^uajND{6m<2e>fO z!xg`~&)(MEc;@gBy6%Dwu)9lns3;r&iBi-Vgdd3>%RO9nmZ(eJ;hU1aPf#CW)(ziS zJ%Iuc?jJk*B~GPkG7$RU^XxYM3S;$%czq>r%8$nWDbn$Gs$7E?(0wuim;%pRYUvlp z?=fB+f$ZaWqH4l7Iq9K8 z*T?I1Y$G3uC@%H4YCL}g^OGoC#GvH^$}Ae6o{wY;Cj0%nv69Z{i|Z;6x)>b26xPzV z^>XSMwFH7ZdUnLjto@IV`(_tsY|(;9c0Y&M2F%l>-4E+|xDJ-cn~g%G8Yg~?DxzNx zR8C|5&Mf&g!>JW8@Z%C)QtV?sg5?miX|BcG{lf`%b{XKWb@d)LmQQF^~1q_W|ZbAD{gb>t_IK6tF zjDL%6anqDzETfgFecn|a?^aZ~G}2H}gdF`=SBO`t1c64#Ea%FE>kJQP?5+y~tLnnD z(RXbNNQ^W+hsRXAGz|`0&l=k(>O~?0mcV1I*u@OEUKB|YZ&xzDo7tHRj;5u6y@e6I z)hOa<>zD;7B)vvBmO14;HgUkC&CX zs-C2A2=CW}rUleEwGb^;Or9}Gerr%5l_OB_*jooOB9b|cs7JjE`I5ocFAbX3B*KJH z=Hof>P%UsHe^=>E>bxX)^KIpq)`uyNmk`S*^W;sS1fu#7;MFO~GqrSrcDtJUW6I(r zbs_itG9r#NewqL~!Wm6<^;N3;4}>jN5;XE1*vEGxO*bBT8-yhpscVrs*la;XhZlapbOv9YV>}on>D&S*nXxLWIT*rx2)YG{+Jm>hHc2@@18j{Evl;H(in&%whr` zM~wg};c0}_ONWo7&}LT2u1!}Z@34iVuGVDA)^dp`x7l^=DeO&(C=4>~Ri}<96cBWP z=o77(BEoD5l;8Caj2jiaAu^~GPN~wN*2L&~_dBykW@Sfsr;@NgwU?i=RW9275 zltZeTegZb?xb);x8(g_k0gQ#mG<+}$Zr9F;)38i_uTyi-nw|zPHlc{8P$oBY?+6R% zatFqn*rJVG{>bc9V{O=))bsuMFT=RC1&ux73vgT;fJ3P*M=O(RIglWU=7zAn0?1*R zNeAH1La@t(C@`qfb8!o7fH`4JpNh{mRIKmS$`Z;f$x+9 zE2zS&NRV!{aj&L(PMIBl9Vmd%U!7qpBM5zSjLf*X`Yrgz{FhVokLSh0i3s>nIDqD$ z;Ya-@mQqy6Tzv@e`3fN=)b|mM$2ha~7SVW37`D>ia=Sdmk-a{H)mjv~f+t@RCoK8z zGNt2r@(ur` zFeGg4>ki5~STa2y37(5SYtAVkv;UZ5dmRy_ci=}?_{N=e0P8+RDX~bUk9M&HSbf45 z7=`1=ZkLvy-T!ti6*Z2G+8ix*%w%`7Qc}6jAqvqFazD)6shiLF&j=zfY`Iq5MoLT< z^m^v@ruV?H+vUHR&cN;b6zP9Qv{02#E6t27GXjTSCTL};-H<=+c1vc^OX-U-okp3X z!5-6+}_G%q*3T{JYL@NGb2NEK|SYn?#a$%Z*}1a>lP}KMDN(42D&9< z2S#E2m%|CD0s+q+;nbeKp6O>Bgl(B$fV}rY=$-X5G$ii7>S%rJDHeitumBH3WVaB( zCGFA_g=;@St@z37mbwoR%@bxVNBhz;CE(zgpf}TQ!#5}RCnQw@o**Tf$iFXWW&?zx z_(PtPXQO_B$4f}$mIIO7a@7K8>%{Q~U%-HVXOm9T<*>m2C+`dGY0#wv8<&id|5_;V zlvaNls)bsWF4v?vy9q=Rulx$xqtMzh!+ET&mxt zH6jc@7i+x66U3)h^c7gWD2HMU|L4{#Vgr2*#(%eF#EL*#R+X$>FX(=W%jNSO0sh`D zy~_dwG?|PvWYSB* z&ZUsQ$Nr2dHZMI8WMX(bWx3|G-}nWkk47dlWtgz>1uhLCiX^GUK(kK}WzER5x9&jO z)VPc14}A@l6s-G!RP36-?!{v324NJ~(bIy{t}#EBe-4L&B!8@s2dl9BtG|7cg;ro& zHShH)j^BBUy@GQ?<@ViRVuBo`tQXVlAoMP5&U1>Lp|i`k>o8@`G!BFxefslLk=oGU z#4ZJn$nkw8n*Ug%eTjEIzW6&%*ZmD0OS0sPn8bZQ0u!wA!e4 zU(&MZ)feM^l5NBOr6;x#lreb<;)--)FUOvsy#Xanak^EYrwOLRmZFNI|D$F`++SRF zTk3#iR1vZh5P?xR2(qc>87c6oq}e7(DTTrZ>ijt97`t55hwXQVZQt|qm`LZ)$#=mu zLmO)K0CqT2r7BbhhY{1#S1xM$u+;C@tG(gad-|)IkAN)8Xe25fMW8ki_*6&5ZqyjK zhj^uq^Uc$(&@eG5MQ0EbK*ly{iogMlT0`$cE4k}$2fK*P(Me81p~|gP zm->0CjWdNj;A(w6E_Ai3xi2nN)uP7{lQCqu?y{x+CW68^RNhB~T$2$|x#Iiv^QfR? zey5jvvcIAzOxN(##y*BS65?-l>A7(yeHt?3L!IKt2)=EGR6tT_7JJgMEFjoEaiPvx zlPL^34?z+WHZBk{Na&o+ol4|hxy@=BW~pO>rp_Veb00V)Y-wCbHuO;xIqpNq4w|K> zip}*8TW{wcB4|YF8_VKr0y{&j7um$A=oXmMB?*`DW(A6{>8iJmYOY|lafBZsdF5Zr z1VQ#x@w~a~&^+ne z4Un@#ADz+Byq2@C1;T65h&ha>v{L!(wB@1WG_NIX4u8SwH5p5@~_RhU7d=m3gSy=SF(ztM(%${7_Ex0DZu84n+y zus(p_sLgM+8_sqtKaiivq5}+r0K8k6;vlj&l*R#4I;jA9IRO&J@KeOAul+&F^qp57 z+|NbJcAm_mqJ?=f8=rmZLhFTR>gXv>$HE?mc|zKxI;-nf!S+Hih_!2XLD8~xGn)-9)T1%CujySkltyGQTqmtoHKgPz zaX7PyV2b)Z37~(U>f7aka9#vb+8_l_<>%G^e=LB<4~S5KO55KRYQUXEC{4cfEfbS* zh|shzqqi)vzNoKxzCs60I#oXQ2c)y9AE$$dwbjnlZ@$X=Gkw|)ekm%a_!N)p3X z_h6X^+lNJ|ug-@HHiK#Ky|n4&=T#P1s)<^VbmB!a*~iHeS@=VmzbZ!2yDtPA(%Wp>lol0+!>91VJ_VlnQ`0dio0RCBXr64O36gdO zAF<1rZPTkwWOV!3R=IGYM<&q$j9X`(<-+ydssTd1DgLKb^JcFS6+Ir(O56kU?*<{> zFP58%adNfpMm1(sni3K#(EZ19nl}pDnCVr)5PD;@@!ye6_e?+uT2%k%>{<@BK%B2n zy4`l=H<^ouIUF9`HqNS*g~``Kt5BKXh$pr-5Lyzdlbx|xjaoI+7+$@EH!n^C1?{im z(p*pZqH}QR({!=leO(GS)S&x;lRH8!J4s1|c9X^o$n0{?|PK|O!# z*`JSzC&dH;YZ@3#7AJrDhD0rZ1MA&-*iE>5`xU?KR75^R0|bib1RNBJpO%_US|po6 z5{=`_{i+>vUN7X&+z+SxL&oE%ddfu~&L>R?k6>T#eQ;icb-kA$I%~aA8;NH`yZIOa z4cv%%l!YI0sL3m?de9a>m6_Kf{k1%Q6Rg+Qkr`GU7IY#%++OFaCzsbudh%?Dr6Sha z>Opi8W4-utcy|KB*Zb}1B>`U4JV!CT`3R0{1Os_I>4nqivP$x!K!q^zRDJA6fk)20 zWo7(OzTuB^v$1bB%qxvGSuv>`4PwcqZtBBV{Ld?`p_b5eimIY#rCavpSRRpd2>-sFz3sMa>Ts` zcb6_2_e1rCCiZ}|JrxN?eDKcRD)zE0KW_!J5$_fEpS4cYu=nt7@s_6B+yf)}!d_wt)>TVKQ>`at z#TtA=uJrI+5&?PRKYpR&4{|NsxKxAfHDZ6@bbbh}JVYP1(P{#WVMw&H(!6)J#Vxot zWS)$nMx%3b3xqo-peu>g7ZBJH`#BGTufLIgX6Ndh{edT5_OVR7OvRK!Q2=_uur*n4 z9z4YwVf2P$@ok&Qg6OF#s3UVaPf@C6vd)h|b*q0P8>wQXxIIEVrEeNXMJ2R(J+@R= zF*Auno?|jH)BB4iXFT{Q8!Hc%8kHL@8q*HV2j51**%~Pg>A782+}v0=5ETY97E=GZ z+EZ5R+WO~3wR*?aOb=fN3TZoFA6X)VBL0#zG{)?}uf#jY2N%s@O?-fNg|escEK5R2ZV=&_?KvD2%f9{QHzTg1!tEk-1^PV2+h=lWSmQz=ZU z)+3)=ND?WrNkaC=F9&yT?Zgl9EgiJg;uASFNFt4QGJ@%mVUfHThA~!v`?d7J;_B=6S95YVriEna>7kNnxrY)a{5GZ3=?jZ~7u#rMI^L zkpmU+LrunCR3{`-)!! z5L!Ousla2#*kwa0{{|0YHC+k^;rxhWB09pxvX>0Kz($!s4r@9~Pd|nX*?qixXie@p zgENl|^dRIB=H2%--qo;@Arbu1)7<{B;E!+KNP!Yr*(;kxQpfaaWWifs&hYnESB*Dq z$8u9jTtMNW$Nv~D_p4Z?3qDS85q9Y6X6kA(@1^|b&|hbJq!|S+ETLh;1*`8~HCmM) z`Q@wsA6st$6?NFQ`vML)fOIzu-BJS5Fe4=}gtRCKC`b!PNOwqgNvCv&fP{1-jkI(~ zr=ExR-QPZE?S0n5wP3LX=08u~_jUa)WymK#6k@)bA7VDOH!Us0oZl{GfOpWTUIcH+ zZI_$(3oW~~EfCCSdyj+6*SH#_&ZV7$2yMg?06nv3viXO3?_hQai-QJVXjK=e;QKSh z9d8T}$*#TY95~t($}qlHpB26Zx=bZk`OV_qWDXzypt6Nf+a`D;ot>j) zhITOD(j7KD_|+3>5Y|bh>QN~fc*r|PG~`aBz|PxtTa_(|x~c(~M{GdMonNs`jlr-1 zk}fo%dSG_Z`ON%w?yM%XE;DT8mjGyVB|3P7XDuBx8XZPe)(yzR-X;iH8TVUN5Ov2% z#xR&P1;0S-|FxS2ZK`51;tO4W-nE}xj>%kBBI-N}ek}}pg=9DvB&|~PKG%Jh9~2zD zE)ZVmOFwT5gV8o{qxDhO6ZZvkkW^s&mF*X7F_tvF`uw)s=0sV zUJ~Vz(sJhPKQsG)^^wMSSj2)5HN^W4N`FccoCzBsU}b>?AL}a)SV&vxy3;#k&jQPu zza`tdX`oOuu*l%S>t*~vPQm=<>6?1)$<#pwwr%p9)vJW=cb%M0z}8bZ3D&}Zlxk&y zjc(iSt}L;c52Tl3#zwoEQl$KZ{=|%()h@%N9|)CZKiQx5WTyDrNj*DcxB1asIvq5Y z6g*Bq7+Jr^VoZ9~;%q~;G`9B@k-$t4%!l?J$Zcf4r_!iC_9t!sYFoOJV_w5NQF}Jv z6yiMr1rr$vcY~=-onN<$?d5Ez)#lt~^Jki%2BlIBa{H5uRiKQ$*_kZzuaKwD_cyV7 zB1a)(dT$I0X36$2w;fQAB?J9Ia9>S(ps`MvmjI4o6m)X7LTQTdkH^~_ zQ&#sG=_%YFOSOwM5YzOhn8s$emN0pAw$x)%CG@U zD^DbR9(@aLhFF)WvZ+C@9&V##mEgzh1AzboYrlluGRh*b>o*l2Hg3``*6BPs`nUun zKAOQc^bWpXw^`R%P3TRt>GuDz9lc4V6H=Eh!`J)7!lx)3H?5SCxH|iWv?Q7#;|_0E zZO57Mbf7)CdFz)wRqwlJb#5sAHkuN6rS6TYuP<2>*o8mn&um-i?AA7&_KWE4;~j(B zh+P?ccOmsu0X_eS$$9Pvq7WmSCKa@U=-HZ%5`qF6_)}+jQ@+^-;=F2&-ycb`jP6Z& z5qwL9kroN#U+O#Y3Mr1eA9vFJP%EkYi^RfiA2`ENdU127K4~Ab=i$o+3DK{|lYU1$ ze5W_xcJggPDwwt9_O@|Nsk(L9(narz14xd`b!E-<&or!ex@G^OcaD`d5D&+P1i@#2 zRBta+R9+_>p~$S(ko3%m1zxsF*3^C=A^-s1XuP=me?NN}rTJV4Wo7OpD=Fxyw(Eu2}=quUp=Ig!Dp}9@x z{}(YFO1nM~?<~WddR?0L01^DHyVA z00}dg{>1n%m%sY2shhG`yXIndU@4Lr<)pA-*c5 zF;kDAohBq!LJ#5s~eU(WN9l2C{nN z8OZYI_n`bhi%l;_PD{K&ll=jv?ay4$gzLn%&}@uM>yQ}2>=c^>YkVPa`!ua)83o|K zePuG!eVg20~JtFP~*g-f)NkrOo2AR8S4rRd}wK59ATVITTsX?58?uG;@x_ckl`_rac!}#lPwyb|W_4 z{iuGpvOZo*)%2twGPhHTz-1LQQHB-62%01)%VA5C?O;QXkqausIHz1 zc1nd$QK|;gNzh)*eN8AC(qXZsE)^@(QGh`YloKhC3fWof~(K;Pm&#s4Boc$qGv<$<<+?}fyV~gs+p0^2{|>S zdfq_awuXuzCtxS^@5;IelqVN;JExQo{!nknR!sJ}fKWz(sN(@Qi(9SQ+S5d4|88*P zCFyJ!+SBWE(3KQkTb=!9?t)s(Xy4@IJVfkIuBT^d&CoYJJ-)O=@9i;zBnMR;b zM4%2+hbt;6VI_UXeM@WZ4}6!GG(MBEaGweJ_t6UiH5(1aG`YUWc(I!OUu|AeD#`o^ z`#_MDuEtRC?!rKiSEnp3-YR3z51*1gYPo=t8ZHt9kMI(T93w1d0i_*HPw1MT-VQy= zN)SI0AtAAEaIfBAV&A6vyx)evh&TZ4?9Iyc_4S3i;ig?`r|SWMrdAw#rfSw4K-pz6 zW;^c9@NqqwJkGAv>cP$Bpe=tlNjLO#^3U|tcAcbO+%douaI^t|$T)zbmqux5jeFPa z?>r5*zn8N^U(xF=x^AZLloj$KR!T>qQMH-4{zqaqA49{2G1c`azt>jEMnPCr)84%a z`GHt;+buoa%8BIYlCb7U<3#Qte?G-W!&#TOT||u`DeowhAl+;s+F1Q71%#zdpRV}- zV8zW{f2%n--{m&sa)pJyQjGJlK}zjp;ZqREgqxOkO4x!befpth&FXB=TP6A1Jz39G z+ti79BTCj1U6&#?P%!mQPqpv+QTF-GX4%6>fAZzX`XWYO@Om=h=8YCxW^=>I1PzHT z*<3ZNPl2F%Xp+LbR(w!FRprMk;gxfmh&%-a zaq%prQ!aQRL-V8R)7mKjk=xHNnZEn|j?8B$NMA1F-QpX>K4t>O@ej~b&c6lejB+{h zk@<}uHuKr^j(YsQ)&dV+5}`Vv%E==!eOE*^8Xe3)Ig=G|+S6>y3hFd(1o+0kQ}UZa zIzST3<)mg8(JJQa`ty#!QLnD;IOImzoKejt)dnFLPFjz)}TvDn|Cd$R(*m;bH*;;|q z{Xn_lLXRP9eISFuln{$@$J{XUfZua$1tt8?@Xxn%swus{e#%z1zS_@a@~e=9zSSym zabc*h=j#pu1Om;hG>RhCkiGmah4NFyFitI$d5;RkRMUpPo{3d~ha-UxD`{-HyN9NO zC^B|hEimAsVjUF4PX1NTiImKfbzsXcAoTyrg?{;PrO2>3v|0>2*1z|zfUzOhvo?cT1IqrDGE)!l{K8%L5O-1z^+pBT=$%EJW76Q2u%J zkhM8-#eOq{-FfO^HcE$dQDBAei()Nx;f-;00JAX?>>Gj;bDK6gY-q?vf7Pv&RS%Qa z;J`@|)erj}NMZ0w`(SqA=is)JY%!oCCDbc3BZD1z=N9ia(t zRF0Gq+^YZk6{}OY-CG;DU#`bR#ptsBsJ;k@!-*GT`q4Ut3Kd!P-mq862QqB!J{`(Wh{60@FW~a zs&3L9ai{L7>=Ziz)YtobjkD@F!ajOU&~S4e#D-B8hiMVmQ@Ta`Q*%VPVElA2jWDDaWS-i=}|RmYr8v7 zExQMWOjr+mo?%2m5HC#BGSIXXYR8Fvw4s#z)Nm~3dd&2hLf{$0kPQ23-Mmeh=%|qf z8lg|XjnDy&k+?AY&R$a=^Z~a3!Z{ykv)M+B@Z-ZDb;Sajq8~VSNrT!pv%j1Ge?h-c zv08o)E$Uy_0|Qv->nF`R(73EL=7*{wOW%ps_Fus5flyhXB`y5zlHX8DMkNO_^k3%^ zo_f#It3tSmd3C;M)kw*jDMp|UC>%CEY*`3b4iUHSV0c-1L2Zm7Hb)zQlhha%!@J#C zQnJx9IY;{wwYFv7%3RFWwR%?Gab*A~(tRd_+y9&(d4T!$Y{Pw6eMm=VlpDQ@W8^3Q z^HMG;%{%XG%s7>@YWM!i>S&vo7`r zIgkVOOv{8es3Py;8sVQmTszvtu4VhE{icdOL*gZiRX(%W2-bi!Od*#ZFTjLLNmz)g zR$IMug7)DmMsKXecN>DF)SE70GI(n9v+|q|I)8tEq_BB#*u12t2AC8=xuaVDy$7z) z-!`vd=FdQ4Xl$L7&GBwL!g)C-trg*zBz(zBvCtz1`%64~oianrtweTcNkd# z;LH*M#XS%y(nfDtonVpo^+gZ87I=RKlzvXl-wwj3oqotuA>$_q)msimm7hP)1=7An zdB*jKGHDXR>6OxRL+S3cIuM45dR{(~BMcs4n4Ch@kklpv)*cchCypH#&`Q)0f zs#W<~d%z5RAXsl9yMhMn)Xe0L4sCFHCWxo5m&O+T2KM(4B-42>0s<~xUT}-_Sh2;Fl(`H0rZ9p z!2JBoQg8j_>Py@r7$R*c>m4aNLezHai~dhWYSKNB%udAfRs>A*&q@hgiuWEhBq3IZ zQ5?Hwp!rCK3W%{UF#6^;cKK&5fGG|bgGPZZo1I7X_IrP%i<|KvINv%B`R7bv&ewfb zd}8zW5Kqa!g43{}g6KsbKO@|mmG#ddl7N&K?*bF;R>B~Ibr^?>*t7UWNq*rMaC_3# z!wd57s2?qIxN=)}w!&Xt>iPVu&1B-~jU@>*yCTZrJpM2+=2x0Wdj*B)+TNGRn~*$mN=i#@ zoUQW%bLBz403z9TI)i>NR9FD)>GKZ@MkFRTv<>OcaP!cjJ*qAr)0p!A$(qSpe}Br9 zI`-aFPJgx}kblVgX#A_J71Q45YwihV&=*|3UK(yqlKGCR>KGj}4&LGu5i$~*ji^n+)y(O{Tu z-}>Jei`z-!oy?cocjX55jipwqzWw3RG{%-9XRlr%O3v@d^54phBM|Wqm`K1+jbjL` zWw+3(vA8Y@TLmV-hu zU#-XgWm332-?WyHix1*qT~LOD)&fhhx}Z@FCx^xPzj(@c^8qC$v@C{pWzCanSHudl z>yT(gO4z_tw5$(S_I< zG7w7xumz?usumTW3B&{3&QW_94QD|)U1djNqL-jUOz5Y>#<(6N8v&Oo#4MEnfZjQk z#se56GdNqQ?bk%DN8E1K{iashcLEO!G)#nc(V}JquYxOXgA_s)Ng&F6&PZs4y{wgN zBI_Xi>3Vub{*32L_RFHhB&^Gtu5GKrPS||UWGeTC;s=qB=4IxZ3Fv{57jMKuFll}F z0u?WVe>tw}Bg&Q@$C^J}oekn(@RYwYoUC|`D_3+OQ|-)2wqHhPv9Q5b68CZp1=kSOk()fsE~9dKQy>+CyNBi*J>y2w_6|C5tkWRx}Vf zCh&2KRjd{;uKXyH{d9yT&TUp>7*A)%8Hp1axL>w|D@jrXJTj7=u!4zK2KEJza4ofEdB4hXM z;{paEgoVZ(*9V_+P?|jD+@<^V?4t|D+lXEVb3R<6+@Zrd$t)>A)sjBs6*0YBK-^Tn zRqfI8DkcnGdu+(c`>9%9kL)h!EvKi;1ziVJOJa73*!=es!^yg)tLt_l$rN^>QVF|X zapL~10H~t0ukk$L$7y4dpu9E*f!%)^IHl_?b7+74no@Z5BPav{BdEvzjX3~<$xd2T z_L?a$gGWNU^a%o3RlP5l{r|v;+wfU29+79VWpe4)uzAC?SCbg!aXTbzKoWHgh`*r} zb;v#NytqeV09-z?op>AxfI_Fq%F4L5n8fP`y*kpmS&KLGK|^;`QBhJFx1O)`Iif+% zjOhb1Q*EllBH}+QsKgXPC0-?j0qr=0M()V$Cm|{}I9wAm!YTr?4O7wSz5v`+)_XdW z8Utp$VH_m8}^??Ihw?5o8!_@KMca3d$1W1{rOX-W7nGimvbj01G8lR z>^hu3d!F0#Tw4N8rG!a(BAT3 zij(i=^+7L-#F4NlB?1&oK1!gCzob3hc=H_-+CT@)Qd_O<_1`{DbewwAr{R53tGQ!$ z=e0b4!f1iPkk#VwDukF2tKWyee|7V{zMCx?--C;vuYa=`)>tD7nHIwQD1P~rMj@-r z(2`+Q_)5=Cgu?zQk3y%kh0nGpyW0v^sNQcax+>;-y;+`Zwz3-nKkRzun+)o;?@Kl9 zG7J1p-{e%gmHC|?yAJ?a=;CMSw@u#|q`rK{PM=1$|~*~#t9YdMmRzbf zIsy*I-0-^wfIUZY2JIW*`OmrU4f;&_qIi9~a?-9@3T9jKxS)CQRW}-xxn({rO@1I- z`2pbn3LvI46I3NNiZtAwaQ|om!1>UaP`B_VE~6F^Yk+{9EzdU&xykEwk&R`Gd|plG zBkp>POyJVfz{(K)!15A!%FN~?;(-|}o#*AXZJj&_3VDR7=sd|f5d7$V8Jl)@GiB?` zxwCRL4MWZ_-k;o(MHltn<*{mBfPi}6l zr({`HbNGYt?d)FX!+O~tnD1pva&ZkmJ|838j4Lwz!X`>B21&hnNe+_p#4>d=5q^@~6h6sY0Qj=yADc608(8URl?q z_E^l?R1$H`-1w&6)=w7JRk2m?PwtXZ(0|Dev!?zcm6jpl#`KT8N|H+Eg_4)#5z0g$r zc6WV2sFBX2U&k@R2O(-CBgzZNK3yXy0I~~Q@`6S9&ljqjjJ8U2zVUG2SXOFjQIx)bVU)gyXX`meE_y8*?~(z3E5aV zAniO(8O%1m5Qb8&-!)SF(RRRWAUg!*-=oRa5AxMCRXicyaOrTAf4_EmM#Z;8O(*4R zI1zry;lkATN#EX@C1j8bUF7{8qOMh?%&o4jij41^Pndj>^TS%oQ}U=8lfNTN_utuz zQ~T*-a4vhYF@vK>W^pv#j6>66iJr9aG(3O^YdSM!^b32_ZOwP_zv8R2e&i;^1+=Pm zUDdp4oJ2plqd%wU461N*vL-#j-kkRJ5h~nwr+WTEmp=Mm=XGp9UPMU;*E4repxFl4 z*sb+|T`X6OGb)PQ(djk~c?MmI4Vk9Y9!&CJE{2lEH+@4Mp`edxZkX{_b+(7qnh2*p zW-2Hkb7l}~3FSjI!5qG;Ji#4zKfz<4E*$e|Nc6hXGV_%i#7(U~dn6%h*1YoQI(zSF zDS>$*j@aC0JwNHbh_YPUyfRtYFZ%O78TBn$UBQK=+&msgRVCl6u8u014ICy!_2w(4 z-lTv-G}I>yk`ia87bt!`u@*n+7rq-Cb@;oy)km4X%0mwb7Cz)95nNbp06@*>ts)4Y zp~7K@tG}sM=5Y_V?*~*XSNQ48L(R2!L$XH@%}4^8$jzSWkEB_Dxx?Em!XgBe4C)M? z1C54wEY)?*>l+HhRZrm;1>Pt|#=GHPPD8=d^jUxT3Q21}y`^E$is?PB_-M}g8^bVs zZX2*qA@3N`xR;!xg3S3yL|bgT=hi1Arx@vXBPr3$;lyJ-4kd76|1YEZ2MdiPc_`K? z;2f6A0=c9}xF4RcK?aaZsVz`}=Sq59k0uMO&c?@c>HtS8F&BAn>-6~i51O-bwVGsC~H$&^@DQg=oayF<(J;Sa+;_d zZs+SDQb#7wuhum)XGlPqy;_lBfsZ$m6$c_PXvpyS&(nco$k+U@@B=L)>6%LZ-C653 z;;)e;*ytZ48cWq|L@yQs!=6r+7Bh{$CVkSuWlGlJ$Y4d@>z7z=fbGTCBu9bO-B$n> zb^p21EzyU7E`1`Cz62)0I4#FCBmDK)uqi@xm&G(|Mrz?9ole^ME835z0Z9}_PB zzM7tYn9+V8jF~@tJut_zcAtH!wU|j5c*2BplHTEiwdp9%*#D#2?7YDd!jG-+ytkfj z8||)gm%3=B#qR01nI?$Pw{AlMyey8;-^0(-f7s@)D|_itqSUOtAZ20IAQ9$__nEwS z{q0t}YFywWalV0`OF!y!RNLvwfzV=NJ8%8h(*`FK=_G@dquQ(5hTeB+tv|_qEb0jz zH0I5#&jQUHMrTyb7`0{8ciXuiWg+3(rCU;_R%4RtDF)X}mQ>?*pJncNC1WeEzqoNJ zWOKgyT8fX`X5Qtzf0o4S{_MJ?hud;06QEsI19#t$HS6eC-W0QrH`5F}L-_P7xdX=y zmJ)v?FMj)Tl~TFez1e@cG6-BVa0G<<1AIW>vev{Lkh^?cjN2vj(F&5bREMt|Qr$lE ztjD_r5jkp%@)3{dBQqE2SA9O&&0E)_@BaX;VmN)xQQ!&6dfaO>1t2sw+^d&?j|C>q zH-F>>Os53P$wC7Awal{Y+UA3%u&?5_wLQIOTd=64q{0#;p}M18?`N<3p>?bk1#JMJ z#cw1#A3TMnnrfG$?C1VAG2mAEHS7a|Siy!&$F#K5dP=~SNK{D1Vu8M{!0B?XrG&E_ zlHDFrX)5iM9w)Lo+lhL<>^~&)R)#UdV`Ls1J%CzOi|vo&>y-f7H%j;bg^VG=GWn=| zAhgjUZStHlfYJBEm{QxqB&%?`yG;jz=DpL^#v79OsFyo+?=VTwx@WoO`tr_f^pj{` zUt}s+iC`<6zxY8Lkq_8E;ki#;xq?uF6|pckmK>klDsAzB3*pN&Lt}>t9jNrnF0S0f zpTjUXeaBCKSu0t#c)(T7N^DzF)+A5X%Ke2dE5A=TTl6+oR;G_zdp`6NG8mbDc59!m zdzo#t*Gd-ZQz-l;8e^|QchzLdGn`wNWbu^%?^$|l?0Fh~P#h`)4MQevb16OZW0Tx; z3}&q8P`u$-ljn+@T482ilvsXHJRv)L*LGI&OqBf3K(R4DO79*_w)rRjoYTl@v!GkxJp7c{JU>sQuVp#VFb_zs3J{&|K7Cm!ZwHRdW_bk2D4l^ zL)qT^2p_A39G|I4-PQ1@*eF0WsL=GPcizB4%Xp<;gH2Nz9tXUI;{dh(AQ069l-wVD z`}2be^#+bfpS-u!CiPmU(xe-R$PuX9*$_oIK^Eb>AinDH=7^enE54%&P1Vrl6 z7{Cf$T=qdkC%2Kx$KPLC&n-%w$)|U5jdXtsTt>LB#OAD{&+hMT-wBSYVM<58+=b;6 zo31PX1S`V1{dqPm|7m?|Yl>=x9e=cLE$olrU7u1u|I)J!Ffc$J8RGQ%jrnhIT*lf( zuFcld)@PI%k`dV=+kU#7TN?Z^cI%GP4;}AJZKHnd__*k*lt=k@A9c*h0M1(O{=BMR zaw^o%^yk7Tt1vj}w<81tfzJPhtPReUbL6}F4MCCGU%MI!(vL#A4m{s}Gox^i%K5dg z#S*i#UWo>&VIp?A_@}tYPlDB0gX;5qyYtPiZwp$FD0Te@z236YAYKmBgsQ7*{ES_M ztc!NQ_~!$h;|c8NDxd4c`Zu%H@#22R=Csmq8h&*oj<{srqp^Lc>7@q>_z~51kFWHg z$^Cn?nxF>PpT^Mg;k(kJ^2a;j%9p8h>xl|nq#nhuIGT?$24iLa?A>MQkn{}SA?-rZ zd9>%q26l6_GDJE&O#asWo7|V7w$x;d&S)-4vfh!>wu3T90tlvW!r-n2#3iT?boNzl zh{A)bUVle>3a~_F;Xw7dQ@0Tarx$|ZzlSN+Kw+OGj3{=8)5Rvn^OPT?r1HL!q?Ga$ ze^I2t{F?3yN=)CqULQaVS3jx)PdkYCQ$*cD^~&pxIfQM@XGx&;Xaw##FF+HG6A!&4M$Mp3pd?nm z6^R4YNnhy-Y27fukrW+A2sohRR{bM4jMa~vm!6M+{H4MFDwD*0y_b$C^a?v#;#?#j z$v70&(m9eNkGddbs*FN?>sMTUWQI~wbyfBF>R-!bj&T>|+?NHHgGSWEIMfYoS{q$&!qo}Q5%zgG$$#p^#Dvl{?B5fjXDmsN9|+8p zl2eCNyJ=%77xgJ!6^TYFP_W0{=v)3!J(qY2_3rf@HMvA|HdR!J0eL=qzqF;tM;+Bn z%*Fna)H2;?REfx^FQ2@kcGuz8K7;0QmADF5Ad#>EBHKui;C*w7#`qcsR3`J3=>jmJ zGU4RP75`Z{d6qJuXbIrZitGNTV^TzbDk&KGif{Rl$_NR!m@cCMX^b4N74SpN0IT|b zLi?Y{=+deGlTp176NLv+P5_~sG zzuM#)Ly5(u#n!Ok+C?&e29uSB!^b!V5n2u6Gr~|N*4^~yuv)GqM{Q>~Y*_|!Sz!s7 zrQc;tRRSjt6j^X0>Egt-55(jR$QdBx>p?N)^N`ua^JQMQjBv5&{S8= z9Ej?sU0CfUrYl;#`SB05zvI^k-WtA;k?Bpb1wC^CzcqK&CChu^_`Mg$T+iu-GNDiA zjaaPR4?kk^c!Lwi-wUsX!l0shcVPvR5x-a+L!QiCDt+`U`X(F`F zTkv%A=Z(^1LhV*>j}@k6+PB>YB^e5b&RkSuPH*!vCQ}|}xwv5ayy~Ij0D9sKzB|-uzC4^3ITEqKU^cKM z=Kv>lK6PX~09$SSXO`E(@X5$}p9M~g@1-2Wv~N8+_u+9MI1#GIMo-ZKTs{ebnV|m- z5n}uG_&0sQeNfshkMg{TkusbhT`mDDCfN#+X}i?w|K(-icI#Tl~KW+R}SJ_JDmz^$jlk{LBLS{$8E^tl6vy-cAbH`={KK zVBROogm%vOnw7u|Y>G`7*;Ymjh(wwD9aI#JjK?q&2%UaF0swGEOoR8(^n(CFp4Y#> zf*{kK2KODW0tS%cjptEW4`xUhD9Fn6CIAmhRdV7=&+=fB`Uv%A{pUHRiElwDvcg61 zIQN6bAZgX;-!_n3SZH%k4|nMR(1pHiytU)-y4GZMeIL&wdBdAa(@@Nh+}3ev-ZNeMT++QCgO9KN-nbO7|81$HjNUIDX{x9V~BX>oD9@vIRCY+u6I3DXrVU}fK)2oY{KHiwFq zjmK$NaJ*V+`+|xq>?NnX(aGG*3wqB`e1JypelHAh54I#0V3okaiwOY-QGaiy?A+|{ zs{1cDXj1hN!~{A~DLa$okXxI(t~_RChlEyoUGb%wvaTQhN@7RXbR z12-Kusgj{_Js+p2pcu$BV{Hs zM|X?Ewg_8dc78JA{@I)h(36~fVa^ zlJVT`kcHKy9!d7@e8iKZjpMU9b@^N~fPM>+1iE5xlMFzH1MXKgl%7GbKI6wc@VIXv zSd3=Mpqd56+N}jN5pPskjd|V zc|taj9&!{*N(Ip+Za{hSN$@`kVR*X$)JlOc_I>qZ6YD3DtDU2J)c4(iPQN~S!t%*{^?sYFhuEPU3yiYe0Uugi}jjZIaJrw_o`#@7AQgybd zxO4{u(8>pkqE%`%gu3>aJ$OU_@~W{dUXW#`FsguW**!#)2L*K@rWm`>CSULj=mVj> zF6Vp@DgWYnx*J}rFXt5|n7^1Iy&u%XVjNz=UgMyCl3MFyy9{LMW1BjPZDaykff+`- zt0vFm&vV%^AsQAM-GH5s_6(ZKH^%^4VV-i{iFB5O?-kMGxM zRt2Yjao;8{a=X;j*&GUHYmuYq4VE0`Hf%sXq@?DXtumYV&p?5D2ZOOq5}fuUh}A#1 z%vNl!J#k?Ay*$lI!XH417zcrX#bVT^k$thOyWlCovsBR#qSn*X!vaHo zMK6pS%|^`7x0kHbho?RoAxf=`s#SuxK$r*uV)f^Q>i+G$ zaG-x9z;Cq8qYPDhXyB|2Th;`7Bgs-NW#Z8G1nq#0B4kfanoxbOQzK#B0RotIj@&J7 zXO{g>4v{}_jR?#@cvW0lTAITnZBQ%8nNI-Ln2>g<``jyCLTvbSJ z+70ZAy!1odn77Zbbu)NvzQgO}V9TTs9z$rj-#{BJ&BJ_E(E2y$XxM z{ogJB`W_0cbk!E-B=(>62)rAZs8L9p(3X*}w{FZEd&a4+6S@^3;y&J5@IPIT`<~su zAuP}Z4SMDFUxMMO;}y>%yd9J2!4%mdP$*c6GXO&mX1DeON4A%GA5RHu&bnkbDFk@4 zRAiGcX~bQS(~W=+qzNz?@gA_+Kw$jI0A#`3J)Hpn<16?8QY(o!f@&-#2tDmuOLo84 zMUx4HW=#_A;L+h-y+M8v4DmL=UmlSk6Z)-X;3^AMBeyR$s*k$?h6$jg*mN9f1ajkT z5R3UUuhGKqi(=#r=%0JT+iC9i+>haY^9BsCY#^}mr4+JUKhGaHSA=|uYzOxbM~}g1 zIZv*is4y^02~p+o8(j7SD;69F-)oBKu&hWXvw@d*N#v9II@_+RMsTnBX$U&TEvD~Y zJXpq;Eg&{;`dR5S-YM2OQB+PTuy;?S$A39DV)4&5;=;{`_XRkR{Smnt=><#u1eR;^X{II zq1|2)tC+~t1ha{M4=5MbX6z4I>PzIM17-jY?V(C7UNyL=$8}BKnkmcuD|Usz51#_1 zqwB&qZ8@-V5qy7!`PA;Z zm|yOXm(cNLQCJl(A?gUdo#t-QSWOeqtq&y_#)Ct^{X+sj2)uSrh_uMn-U`;~XcxEA z8=0>(u3i=w!iIH7*fJWJB;DwK%fDILUBmHoSk*F~Ulq+?7IqVVo&jvPM>QH8tTNNWn7B2ytOG6AWQ{W?dz=_!(3; z*H=2{P7jxs4$9_@jIv|{9L;(Wb_qg15$KKwJJrx^y56CvrAU~SEaPaCR3B_AMAPSU#Alb zjwr^QM-Ns@)DzPGV4-a%bv`o%+8!e2f&YxG9%%2VxFoINtWbzFhwSn&R^!%%kI>dk z<9n@d@1BH?FfkJ6X0If^{EULC2oFIK=cRMonPivq#Z+MBn1vH6w}|&=)zZBAyadGT zTr|+tP$i`?Laj+Br&l10F7Mcs#fza`j3Y-8z7ntK(h>Yp;pBejeWAv-0Ww836Tc}$ z!~846ZR$fwI57n6fv!vo>^lEzO2Wx}fPj9710wn&1upF`4Tx~>I{Y!4Uf#3+fh=Te z{J&eQ1mua$0y$C-3?;Pj+o&U9%~!e~meFXWyd?6(rvEl6Vt-^(hACmez}|%~0SR3v z4O(g}iMf47{7pSdND-U}rGG!NfJVF2kR~#eh7abT!-0DWtmD#|R;?l(YT%c{Uekcr zUl|eq)!}c9li(^B#@cQ%GCX*t13!@&5?mFz7COa7sbxdm1ajHjRX0E zwhX5b1xCzm%oVA&#KFD6J)0^;XW`Vv!)Eu^r{$l_t1xd`Pi_>gCG*a%J$I^7gT#IU z-<`|gQ&?$E9FCKQyB?kDu7XEnX_;v?tPt`)ECBryLGBXHT}_8?{z;3Ut7>XK`HB3T zQoL(W-Qm1uyR#+V$*tQ(bZaH)C6PU}zW+I^P|A*Tsk$0sNIbs$6#&P!f7C40KIvspVOo)97M;rH|LW@2}M zLE`$>^6Um7FOR*;YK4x#1qg#6d_DN#`*TjKA=A=c5T3!J0a z{=BkA24n- zrsq_=w0dyKwE*<8p}|48H2k6uno(l9+4H+N@QahWevihNEvNL1*|dYnE~pGb)X;%S z^10?_FS_$~CMobl*9}D{hSmCb4*>1b%Fbx{D=8-eS9~P&6hv&zAR_f!?o|nSG!*zK zs5gYmhai+U?diSi>7`>x3;RbDo=STwLI|~Y`$xRmoYdd@`7IseWCccaWPW~cA2#sn zqCsa;+mnWI*v|9`xPQ3@=qn^Z@^ON5Q52H`|K};uFW*9_Zzh+nu2L6`H8f&GqSoXZ zfj_{~pEWr-*}S~k`2n^Zkcae}(4#+zI+0l?+bf5vn(~E|Gx5t5QlNb*U&wRsxX9y> zAOBqbTBcHZV(e$mWZc}E@^?X{t%zOBEelv* zC>fMKeQL?!rPzIdTEN;Klw_%lXfbwf^F=}X4ZT0}(|$UV$xG(W2Dvj~zXy|ZCK(=} z(Z8lcm+#t+MOl;Y)0&jZRA<3)Una3*|D3 z_UBwEZ}REZp^l^$H4%&}FE7W>t$2^8mj);%a)6u7q^72p^S7`94^_h59ZUUGY!ds~ z>HZ7u-JViKhJL_aFPIH%Ym@p+EvlipxfK8aW23;V8)tTW_#No;qO0kW*r=XiK&YeI zftX*Yi%*5-4P z{J*r*|Ar2p-*g*EXFkDyN4}*;@gNMR+f1(HdBw8i^UfC`B1Q(#nO6S8t1X|@DeQ!)(&jI4o7Bl@O*Un3nif8!wGT@C%=!l1ltJ>8o@t(2adymTwA;R!U*gU?(Xg zhjVZlvU3!sAOpIMsJ#HE(#*)#*4D&W34q}Q$42mM@`x3*d%?@EKxcDDcgKY938^6# zT5pe)$4@ro+UH1hMSe_?PELKe#J>fmTtBCrIcI@NqXGsyki|1(bcs)d3gLj+4RF^Z zwX}#qrA&3ZpGWrkd|&zFf_;!i&?Bg~_dawmU2OWDU9I$B;g}vrCfq(jEW`smnE&HL z1QivkUTjY!z|a5g-`_jRG0f)K%4bJ7$2k>+fs++_A_0ngHHr`Hqv&9IqL^0rrk~j# zV?ae7p^~cEhaV8XaZu4?PmUpp*N9fy2_ROIbWbi*;JaEN=^q;8bN$??L>h2^fQLKL zIdA5W7C7!qhWPq1>}UuSKhHo%-^nCXDx9}U|L7dX21+Gk^x@&*x!{cPZpTvD801*p zN0@R9Y3SsX?rZ@A*);@+{i@7GQXnh{I4Q~Qt^x%kk!Z84|_U}Mh~ z<2IYffw^Bn;{)z%DIhH`FWLKFsenlrR3%-2G*iz0M{}rz&;Ru}CbiM#Pz?Lz6>OdF8E-ZzqGs)w)W)IHp?| z6Q98B&(5W)DCv$^=VfM6@4s4CiZ&sj=mC%*_GyrZqz_>h&W^NX@3|E6 z!&t9}7#OREnr~Y-rxvK_Tuzpdx3(-!E&$LALQu>)US>DZx`M&Ii~m|#J0I>hB7PF? z#A0`(e(Lq*#KD?b>7yO7F75w09Kh(96agb*aKZQMSFSZ|2n9}kWGFk3n;>_P%D z)ZvCqLZP20b2B9MOHj=d{4Bw*5P;~`bo14{$5x5#XhBm7Rm)W_AQZCDw#?6e<%&+4 zY7a^}bO>SR-OeD6d%(!SVsluqoahAPj8r`|wD`AC-|1F_dda`+W|r3VUoEcH(FPgd z9UkOEd0|XawH1c`#o=|1|+sn4m^G`EWM#CUOiQKoqzx5%(j3#|L^!BQCFq{*lVa}7`l8L#L*=D4X9X2)Eq=iCNYE;N_GFz{ z$k~)R8aSAbbA9ouzwef42BAT<;NJh9-P21dQ(2uv{%Z#b`~?E~n$D&Seya5Pp+L3# z+QWET72Z#m2~n;bdi`rwPp{Ta8qLCYWE*y?ujKuoweWxKncz!#*5xW$P*tFwYjP?H z2AtO{B34zcZ6hHIB81BPlUO*bNMDgYjxdSNoz#hTh2k=IDhsS zHUwc|#p8|J=xWE6Ez!otg*e1}>bA>O?6GnX^fh^p|D7m^4EFT&+%P))`q@h28O@#r zLAMbaf%{O-@4}H?bSf+L+`6DYYdsUdSFV(tjIoQ@n_F4>_qTq`*xY)Fa|#bFLg6)t zG;}=Mc`-EJfQt$#mi2yFMkC=CN6KSZ*qHW@`-hENhRZ6Z+i@SOkTn|^=UNdc1{-T% z3V;&6d#eMe1qSjUuPTB~PN86{0lVe@@nIS6t&FKYGlUzart8w7jl)5;6EIy+cs>Su z@_xxMH{%z6x>f??wV!tm!Sa{2E!9d`|NA%g{f%;OxAw9oU>mnT2euumXX6mo)Mmy? zAQObd7|TG_J|6799tVyZF;gj#)w)~_pe(Zuf)B=|Z-KJ45I}$?+q}F~B(T6Sd5ZA} zL@W|E-)aPb@&~liy9w77zYwg%WL4y5Ae!h|;@zgyKZd%f4wFuAZN ziSMOJk?W>N=mELh8KW%62hnRV4JS>|xKu-Se`&y7skRj@9DhrrrT-#R!*o|uoYEW~ zP%+n>jUNGYbOZwfLr3j*fb@m$tgXE{Z<_m+NtR7aMmBxUO;OsG&z-$DUmtmDc-V5H zn&UL}BeWpHEfmaCbH{gm2Q;7w1gE8?t+hAM6@ZEeqW+zA14$O2r$DYP9p%p~Gzf>X zcK{0*n7MIr#5u~dp`9S+W*dAnAJ7Iz3Ta5d!^_9~s$Z)FML+Yxb2tP)a1GKK#`pi;MgI4t1blMNy6h+$2Pz8;PovS<;;+Vix>@*Mt`;^jNrpssv3h?0 zd)~&FL;#U)H^!_W|NLg!z%5wlx2dV7FO!lecZdez`48-9CD*XGd<#GvMGx~*eWz+iib?u_Umrnzt~&-x3qx;7#$=ka zc@;3hKPs;9e9cb!#Dv8BUl+9h??VxiZD;pq1FdwJWRyX*`DYiuIgOalCX9uMk*P_I z&@{IjR|7Zp>-*i+8^6Sfd@@!0+5^7H79ji!7R#eUwc@40Vd)NBZda4R zusD2<`{8^~Z>-gBMb?H{2IR-iplYRtPl5Jw(~$NOYsA^u`Ag1NxvLX$s|Djx&57NvK2#oLemjT zK+4g96;c_qBB`|VwPF$sfxFi)Pnt(P?~qhgZk|dLx)^1*Uhi{ry3Gh@uU~XdwcC7i z4tr=bimm>i6;anxKe)s9k<+`yrrM@0;;XS7`3X=03N95Yd_dQJx<8+IPesSV#nl#O zzyr4^R8-Q3o+m@Xf*=u!gNsYBDciU+u){4t{Q-w`I~L@Z%NoxAL)TYFWwmZ!D}t1C zDWQTO-QC>{(%l`>U4nF%(jC&>At5dG(v5T~Dg8a1quz6V_kLr%f9!F@Bk$hNUNP64 zbA^P5V-2S_tbln5jb;QFaCPtSn+LW9{u-Tbr#l~;x=41_rZp856!g~8T@NfMj!yG- zC?Cb$y{;sl1n>t{u(h>ymX6Wna+qi2b8d-rk0W>^V`Bv8POSa6pn?P4Bh@EhS}d%U zTA3QkNPEIN)TUW;X-a__fw)?=dDOCfq<`C1)cr zE|Zs&Lj{mn$bHNF1r7xzu}LGYk}Uy772nl^eb`K1K3bkhh4wst&Q&u)lXi2xx|!n^ zFZk;1r^t<+hIA^v3e)kLatuTb)u@iqg$1p?qTMw&8EN5Lu5WHK1EG7fQFGF{nkF=E zuCBo>?D1t~PprKjmuWT#SR!o!19T#8z;vis=d4vX64#YKxX+frxwVO8A+JHXYaE}`vG*!Dn`J|^^q?(4sJ_@2%C|?!DxP%UgH%k^#^KA zPSp3RpZ?aLp%=gcE7x*h&>tRyDNr4FC}0GZPNv14_guvZUY+fmft`+aqj_d-ZXAvW zn&z&eX+bDdDKi|spxWDRIbV#eD-wV8>Qw+@I-vL~Dn>$q!Yx?P3EQp;i{B|!LHqmh zaklfKYv6li0(EWHYF{5E`0vLT6Dm672M9$BY#KAm-jkJEKEe+x<53(bHZ4B$dKdXU zm|}x_h&@tXAdEfuy^35TRF5A$RMIdyoQmty*RdI`6-nBbwMu2Vlf96#rHpXW(lL`{ z^leRb9_a%t8I-d~;uo^2!lP9J&wkYmd%`boC1`FoBaf@831i*5p+^ND;VL$X@#>?I z-~YmYeSGXOY)uIfk{9nloTt1VORmmg_?i^K-?}dB!e)YTG-VsXr`}f04^h%|$MLQkL7lyUd zX#R8#u6fipbORzLS!b=qluUhyJcZ>4H9)xF1~yauPdt|2?!4*td%fq3m)@x{ZJJH7`CK*zv(h%S?rbnjd8VyZVxFYgAM zL6r#-x{AC?^*bMFxbJN@s!{rd>ypV-Q1ATss9&~c@z;8FSaSiAh8Ci-KusbapMmp8UjQ3UfUmsQwG&rdC_P108#!yt&jA+`$JA14-If8wk(CQSVz* zLW!9gb54?O9i|3juk2I1x5LYD1m^W;Dm7g z5DG3+5O6RgLe`${4&pI1F~MkZUGDWQf!MA+a*Ejd^0}u+)cd2-KqcN|NyQ5*Q%S{J zH456JQd1%iS^RJB?8V6lZ=QixL5Ata= zVoWMh3cZ@GMUeqV1C^)^R5c@D(kRU3@?AGKanO2M)xQHDl`kU?uDm_r6(Y6s{w&EV zAf^d}b#7&aBzoGYwq*A}ZYIbLp}lQ7Xi^x#kKruisWLIK6LHr~ zK5wPjtHn8$1VJ^==N=-P>+*j<|6Tfmbxrs?bH+|B$J_boV*o53gj%N6bPGG6?&g71 znpjlbj68t)D&W%%x7&7^I*YU=minHf6y}M^iq`m*uKdEtl?~dDt_Xu_`*mea8z#zA@#r#M>>UK{K6>=1+qTm7oiIU) zN+_o)Q-@KFix5q+2I7td=|ja;%u+=}y)~Xo4o|h#2wu*5X_*PvhKL2$-e~F+Tfmd47818jCg3to^% za_8qYDagpm`-godup@l=+9jZ;$FxADQDeQ3G|f%g12{@@E-v-)#_e=}&g@HFUMNd= z;Y)R4_>^JtW|=Wc#y6suFa8r7j?r!52>XHgV8DX_l+Zz#TCXt^4{t)gA4n(6Q%I*T zmmQ#Muj_+r%chA|2igV$cFFUbIyYFm^f@EGr8guBDCq1aj>E2P)wY9Z3?&5ng_r6Ej8rBWDvj%z(P|Hak)?RiG7Z#et^Ip;Fv{KEF z-`rjuLMn|U^=3q|gZc{9pMJ53hk1VqiQ6jA;!q1JkX;`}X=vp_%bu^xit`TKzUp)*FaI}g+aVC$2$Huv>j<%ctl$WsHO0|TLKl@HE<7ZWBL8liug9dD~FzFOFHwV5KJ zK*jzidO%me$gFy?H8lZS6Cy?#>|RHjfHA zGrn##W;};ZNqXSzOB1+PGy{enbed#FKNi}X-UqqZcJj<` zqV6|WuQcZ8(e){lnCpApqU#V?I>H&0xkB8dk1qwc0Jbq8O%s3g5IIzSI{bbe7nM-S z^?NSxY<*mzJbEvH2hSL2@XT5<)cG)YsRD9b9u`tY)mv9vnp3Q+f%r5vdC`%2Aif;#9}i?o#`Tv zD(fRrp5BjX_gejzh-;ncPII}M;+N}$tO(XR;as1aU}Tj~AoI{E1cIS)VJJ=F=4ME@ z3h0LDsMRX{K}Zz<1fINWk2z1{Mtnoq+45_~uZ4wNv`U^Xg*2QaEPIWZwU0kfRsvFk zAh7?g-JSpN1T;MxaJ|@i=GHJDerMgk3h)VdzM$0|*#~E!`~_2!<$mZr8lel|@g4BY zlSSNu;^g^Mq;;z^bO5W3;iqSgFPGh*-%KJN+gUmnG`f5x;vj>UXXC2)&K|Nq*>S2m zl|@h`cK9_he=MUZaI7()ZM4=XIb3;IzwO@h8!YJ+bzbtUlW!`a{OO}g4Ye}t39wXI zPau#^;{O>{_?mje6kD+q#;f(9?am9$mHa+c-`hC>HnXrHAyuf$bo?VxuPb&kGO-UM zk&%)4$OIfgb@>x>QR#X$<-SXc8mWCzjO%wSCA{k4wC4AI%c-wmJcd!Ge=GLHurW0JI^z=TqoCUn{c(M zW#s}EWhg>OS1orlm*(56Y5Qlss~uyTKA}^g)yk;FFxw)1NW%Ud25IxI_yU*~pKE@w zQOzbF&3ZCz(O~BkXA)4>nH#SSF9OyaJdP>2DcGkYnH#!C6TTSv^e7a@ z67A%;H}G5S2TAS?K=X$PADx~~#L3g4ZcP;7ymeT6iKxJ4#P8c!ToyaWt|Z!!u9;6e zaJr#z5D7wwB}qs(03HajbN6mPkF}T5fsK>618;t%<5QVaBS(Tza<+bDc%IADp(r|*x|ia0CMTjM@DSD zzP|SV9;)c5y=44paMIZl%cZ1p0#jqi_w9cJNPHO-(922r6cAga-|+_aVKSopG;NkCh8Ds_cM9|`Mi@xBwW*J)g z@zC{3?T~tE0ABoLh_@|=5mshA<@A`GtysHZm(O4bl}tfR=xtrJV+y4Stp$s7%T{Ba zsBpc`${DghqQd0AYUgFN6waUy)| zz>10o^&hbG&tZZk(X~W@k#WU8QPxxYmGyNUp;)l8?)P6*>wbpH7VdGf;Ey$45C*tO z_-smrUr7r7lG~elPWFl$vh~e16|-a&8J#SEFDf-oLWFT5QZa}yt*j^SYO9?v zrFK|E?cyQKZZT@MkB=4ncN0X>T1_y%{ZBRX=X$vwS|SI>{!~A%aSV~(5#PB__1W*3 z;}Z`zy@z+ry6Ezb=W^>+wh^+sZBDtrK4jS+>sg=5cIdMgheC4nt$Ng1Gp?fw{!ZxnbtL{UbBMY!5hlbVuflLV1_$Ci4bRUi&;Y;M<%t%~=d0Om6hW<~5PZjc-Fo~@q` zdyR&DA^Hr8za)11yu8IEm|CF>WYnwzvp3w{r=LDM;zf=%I_`c#c~AL2UXs6Y*LG#- zC!(nDpJfQpm-8Y2D!NlX_(=Pc0Zckq9IYSX&CSOxa{g97WwzwzVyMwhn5^WgHoG?F zdnSO|VjMrfjWxwaiCXX$#HQ4ky9uJ}_GZ4UEY6PSW8PQKk&R;| zrq3ZTyd#%)H#Ci>_~U^GUN^oWG?{~IPr~5>XvvftMhb0xBpk^4Y2UcHU*P(4Cbw$P z&d=ByY_2S9-Lk&O<8UlGQ}Gz<3+=~Gd3!#B0LK*0LRS7%3|Xn>DX~r@0i2CTx}^rejZi{HCZgs&jWd1=3AWR?UTDTrddbFU88@$vg;J3s z>D+b1$Z#mb@5Az6nd;5^2M%QMnE|c8P90#s6obSL2(w!N&>WRJLORL4+TfKcxvHLk zaJm!j>wg->HfXHPNIrNfL`C!CWKzA3ZpZQ)!fEDq%?Xu0Jp$r01=<|BFY6AgPAIY{ zN8f+MAy>Q#I?QJ7{HiqPzm@%I72cx6=rBpEUyjpjR-*IvP}zK%O0$qLe&%>iM*7r; zoE4EjI&VxOV7GfJKDzn3Ii9Ob3HfSrFYk2U8wW(w#{Qf=9ti>usABiB34`SRqV#S^ zD0!BowQ}hR)oVp#i98n??iQ|~^QC$t47=m7X-x{AUXde)Y(Co3^VMlGBhVMxh_d>J<-W$4KP)=~kTTTEuiiij*ZzyI@qd&OX6K^>}UgTixMD zQcd~3d@~Y^gjQrEr5A*3-P1S*gu}GFi6Tg)qg7e z>VB5;zdv&Cpru-#KPCV~5mYIpUq9qToWV^}ywP>MIh>!9Lp(%w-f|Ho86x*ophe`g z&^3{b<8q4dbB?^GVR(?-Pg{p%;uJ9+XJPboL%IG2!*Cyd`_4hk;_b$sp^{RDf!v=$ zMTqjN&Z!w?#<*kEN0vaV8MUWZOI|)j#TAg-|P@XOTUMGxEcmoKjKC z9ZXa)_hH!TpBcbSNRU#9Dsq(6?_=IF0FVsdt>&%%mmUccZ|mdDb1)k$ zfT+F7t_#wqB@9_)j$HH$Vm|v~uqFJmHc7N)5zu)GLzQt*%pQwA{EZR(70S%f9(rw) zaMuMXA4q0unN$@xOQDAEpu=(8Xj=K7`XbG$6<&P%WxF+HQO4#R3 zdJeA|Ok>|rH{05?diZW^e-qAr{z6jl0FOVA{!wKR@2K!}@Q?A;H1& zJD-wA5#>R-`H3?}c#5-1IL?0atJ%|^7teTPnUTo;yny!?KA~v6)Xg#|o{nKoDKw5I z4QFB;4dSp`vF`DQZd2WKdQHD?o(@!Agpe+JckB6ss$X3m9}-%55~>jZN0O-hnJvf& zVwOq^?N0^X>QzjjeP6l#u`fz~F=*7^H<*XoHzfV0$rQoH8dVfuwP`BLl@n>7g5YOe z-bSi^A)xZ)7JC@JHbHHA!|r`j{DG3|wcqTCmFPo4UlIGO$Cnh2YK7|Od=i4p3>P!+ zJFT%T-e7XIa2IZV;5pf>+ZCQ#t5PAm+F?E_!_jph*Z9s>{uo4{!qH}CW3GyCk^k5B(Sbg+F1bzOP7J}5Ao5G8&TI(5-a zQvIV#2~Hk=i${e29ibOct! zyq^peYYi(dl#rD%a4dE#sQXz!Onk^C8X-ie5=!p?^>%Bc^JH9uS8i2&|1+`d*#2Z1 zlir4*xYUX)8Z|B@w!j*Ukc6Xf%maQ;IPQxHcX4X;=qu$oYn|4Q*exfyeby*cF8&-S zhk0g-p@Mb{mCRO?WSWu#3h(=x5{lLa2CDa$_|dX#+s55xLYLw=jGs;| zH0x#;WJHb8wb+T;%rjk^dGM9h2DeNwU*es+Drb*y|Jz zX}nNrkX2)vhhJM|nXV!#GI>%c>eh)DEozmbJ?WdO601ApKu87tiG z?<9$bK9ZKD^fv1eEV3LRRNB?#N0K_W)$Cy7#`&gD#S%4S>RAwIkLh`Ug0heKi!g`% zccxVCsF%gTIoeY6vp)##nv*YYtTOV_N;1B${MaDDV?N#KI7oMflOH%4PS14Tl+FgR z3&RiFsbV}&r*OmvhRVLDrC4+0K&_$27Y1hO);`lvNiUJ%JV`~B|MJzin7lbyXRxE9W{61Hq@P{sB!e z2^&ioa=x~~mAqKa*>{#a3&eHYulN_c8PTW#TDO)OXNhAry*IC_E|T zid1!b3KrO;b9*UYd}PL&wi}4m;gu~~(h@<+!d0;7)0asxbPZs)y!3iRO^zYt;^O+D zaXzatwWxOVF70m6a>+h$cw|m-DPuFS#r)*ShnC)8yjk(mT!=v2z0kwXjv)Y$VvMG= zwbes~zP<*Iqm1?a`nqSRSvk46W(U#$Pt6PagO>hPEY?#z>_d!{k7@<1hTjEH0YB9F zS$M`z$_58E`oNBNT-%55@M0tL&p2yvHJ|4_{{3_1gueacD1%hQ^z`!MV*uC=MV?{C zY2fok>0?)Zjk#gAv`9}XpeKpv6?2MF`*Fks{J8uP$QX;ON}6;zjGzT4k|VI@yMs`R5C)6$Si^8zVQ~C!dAPo^`_3FKOI*8 zD_tzYeqeOUY!8VXQBNLW%95jb$Wvf4MGV^PzvwIGQ&QvJ0c#3_Iu`Tmh;Hmaw7J$e)ZV0eA{FLr%U zfm90ep%3cI^6U766bkHAx>jSI53cIIgusP9Zgo3QblZ<)m2M0b=X%SZwzwKcJzOrC zp__`Dzb{P?$6S^C0kLOTQE^+_RyR@Hn|d*0aMWh(<*Q6#W4p&K_IGW}2N6G^oTM?) zXM9wDb_x)0d{#E3Nn?|(@=6kPT##bxr%EoABN5UICAze=+kAWZbJSUgxk+z_AuDh! zprq~tdL|h`&S3C<5AiOS`okDolD?4LR^;|pAQH-mE>2P7-A%)v|EV4%J0Pq1g^Yo`#1!&R*tm+DJkp5_)Q0(F?u zQY|KaUX4Q5A{ip{B(-Mtn*e4DvGFz+5qqRNdX+c2;SGccl#-Cv>R5a9w1;EGZv?SiL@2MF*t;KvfoR5~ZmRNc^=sSM$xf%u+EyRO9 zRNl+Wn_b#NFGMppM~h^oyX-}oLVq5^9@LjM?FZ|I#)`p)r@6=94@8JCTO+OtJHMva zg5P$D_eyv{h+Fi0G;Sv8iH3J6OXUbIys~0gpr5r>+8FtUvtDvA~foK zFw;*Ia@k-c2O0rS3LlkH$TxUEkjeY*;c+ zQtSQR9To|&*0Y$A4jpv16p~uMOyp4l1BkG+_F>3o<5!0Fc~zgm^z`apBIY&I=unIS2#8$V4(3%T`t7w7cetw^ zlMPh9AJNjHUA_8)oppx>b@Y1E!_jYdsr=L=Q-b)@Au{8&LqfObcena$=7A-l zU5+;7vlqNe*T<6UJhm5w!o!NkGP9kB$75BbOOywlD|NEU7>LSL@ZG}Q2@Z@iCJP-) zVQ1woXGA-61sE`rSp`($wpDLO)rT(C3(idJiVN(3ur&Lr7Jm*ZwSQ8cG$VX+1n4=IW8RR_1+Ci2La{vg zudaIeC3G>7P;gin#B$Sr?bZ)vLAv4Rh=W6gK-t{YnEyxTnEbLtkq>(CkFnVc0+5fG z0S6|v8=oN1A-V}S_ph9mK584@5+da-V@w&vK}h}TqPHv~_}|_%mOV*us~^+@{X5goG(Ai` z-zL%)+vP@LDZNCFop!&BpIW3{_Cw0fY~Q)n?VI7g)nopxlSq7+;SkKZn&5<-ecVBK6`W+})Pa?{nq!%hZpB@YVRMGM3@^ze+ ziD%NV_pG`9eA)xx?o}C+dT8AN&mUM~{o{{*VV0S2keV;%98P;;XlRcS(E?lps&`E~ z`RkcCY~Rz*b9493E#;39VJ=?jgtAh=O=opeqb%jcNgJtywjHJUv;fuogZU;$i=3Fv zsNYqwmot!`MxJlCz;0kP@vXFLDJV-_`z;IpFX`S=wXnv>1?mdIIOp$lm zg?$R|a0wQ^a8Onqk$%a|j7E^%N(ao)*lDRRQ}}-&1#)dCL^cH2@`mVQf2y8;JigZZ z_icqFl7LgW>WG);PFPWd-wh*Qh_b!lN$gLpp7;X;VrTj~9w*M0))i_(v0%_?cmT~lIq|- zAG|*y)I$H*%TN6vdRG-ffA@8Y*$=)UT+z76VbS@G-NMfcG72Wus9u@2yjH2T8^3EN z$GqYV=)j5bla;IMR^lDGveQ=97g^8iPL(hBZyfV`i)M^+jmh`zj3Xh3u{}$%p(5Kq ztVEe7^}{&gl%U0F;vxLBje2#0m24Y({O{IrPQ6@jI*Bcq?=Fw68o#AGD^m7hp&@=$ z_2|HMx;-`ptW;pxdNI6Xjw;9Xnzt^{Z_G=gG)o!la} zD*zh$|IZ_$#3&G)^@gPj#VZe9?T>^$L;kO*2vElAR@zS$QTOKjJU^Eo8A0=`Y2Pd& z;GX{6mq;x#i5b=^ME3(y3Vli}J26KPBVxOQT9d2>605_%Nm1$$Yd~Jk%d#k6?)dia@m^Ke{;t&(o2AHZlEISHpQo@ z!^;qdK|Y9MaK^e@&#<*d6S$#tE>XR{)4jy{clZ68~Y6=pCb8iR(VKP=m)s zrSZ1yK=xR>zMHqCGneD8RMZ6f(y#K0l?;97sa5TCUI~99)YW3<2U?XEDa`s&>?dsos1@s|j;dVlXxuZWdY6a1JQ=)gTC#g0fjtaZUth2n6(U=!M!p_ulvE_e zQ9rjTdRr4yei%1z$YR9bkpiUYiwVx#i>~8LQ^_r26C+N1u(4B5(m13h2BMH`7R7?z ztl@fXs0IBvgRq7Pfd;81(Aw3I>EiJvdMFE|H1NaS-1%TE)$mhjex8SqcNwV=`diH{ zYhG6e^TTz1t7Hx;a2h?bGX=oGcHu$f*fA@&_}-!jc8M)4c1AfT9x}CijWX3*d3=Ir z+hL}RU}F%=1#c0tiu4`smF#^N!vu0i5989%{oGD5mPP(#i;H9 zKyaZz$MZb`IQg1|1O<&)-VT`XWR730GCP*Ofr~2^>?M3jEcR8I!tvNv1sckO5Rn_m z$rKCe2OW_^5mb_No4wV88On*i^~`&c&|S@>@eQG&P}5pl)77emOJ>Y6_ysoJ3=N- z4OeeU+`OXymiNr`(w?v~_p<<;#;cW)m5r}NN_V2vd@F9XvmF8i*uJ*QS9WtJZ;6KQ z?(vBWm@C9Y7Kz;8Cy2pm8b`7r<~FEp<&-I$x9m#e?x31ESIfvSVV6(Sc=vg7>y&zW^c5j9@aZtP)l`PPo!=O?v492Q zI6xnAaWSRlo9RCa@z5C?8(S+DdjwUT;Qv3e<{uqD%5?^G_rnz9?Gm^kTbyIsiU`JTKB`b zgd5?#G^2x^+zI2~UI6upB=TeAJDhKQ?1sSnP*JlkVMj9Th;@Nob-N5l@u_32g0Q8^ zeiFx%11in0AMjcW4Pn;0v8n=?Q9WNcaWYw~=bXv4Sec_zKA%uA>JY-E5L#m+xop!zg@ ziUGTEH{u?NR}wupz3}x|QipJc8hNTOb7|=@s)mm|EH-^%0P{}m=kq5jIN?XvgVOHj z^x;>eE%8Yt_Sk5M+EnnCt{Yuv2E>RtR1JBx=Xz68*RF%<13T^MbJrJE`#{{umWo|DV49ud*`5pO0Yd_9~&yQScuu$bSxjeQw|((a2wm zZ_AFr_SzV2*LAGy1*w~{5^pQ6f=LjlHRH~m|pk-+)e9#fw#V*a^Waw`)c5@Rwg($+(X|jEbk%(J#=YSyWwU+wWrw-e?RaS(fBsJ^OxFc z5f_Wo`5-44gOozBp{Yp`IFcyTD!o7NY71Oz$JPO&uGcsv2}d99rOFIxCFiV`({wOJQn|u6-(> z){SZ}fQkL|`gGwjNAq?@dDLjFH|!2Wvql3ppA=0-af~op;wr6zz6X&Od#aGQLQ?X_ zS5#I7TH|r7k7qSw`4($5)sL5S{5i`2JX)B&reCnoF0-~>Qp1&*2b@2I(6c;9HI)l@ z*PAVUg|Q9$PVIIO1TgLOt3;o>`UAr_^6YXqH(-d3%QLUL{hslp=%Xd7OgP(u`i=SM z>Hcv`e8y3Avm;4q)<=eL2@&K6M7*kz1u*j)hNt2F@l$`lZG8KL%hpA?M=N<{Wwg|) z<$R1V8jEZ#cbL*ij)`jrBnsp21umdfO&M;E^SATsymC(Ot`-XHU5PNAQ0xb?v%^bQ z5P0y7ovA7%v13yyC=K|OOGGd zGkqI?xs_6c#h#lX+R{3n)<-`5(9qDZL&R)3EqCu{^3Nlr`O&8-d68L`V9Yh!(THoB z^S{10kQB%J4Ed8aBl4_0K|;y}zDL?s4g#NvA37Cw95`Ah zaeZL%tV^rCtrvgWlHKZ4!c`qVA6vaQA!1j8Aq|i6(*!mn;r;72 z{C!LIq}%fZFc+V^aH`bs2mH%XVC@)p^J8TTFfmMrZ8*(-F&icD7_fkN;NdrC&I?^oPaZrZebtVw^mu{$O&4fb#KU4F_6~YF2i}1`Qc+H| zJa&KDg;Nx;hb9hb^N$0EahX}n#{7ZLhai}^F_~8h2eh@djg|vp{Mv!_oDMg$5n4CM z6I9T}4Q~K4squIe3!#HWAnK7}4cVXK-De!5eBVs>84u6N_34xjaLUon0{Tdcj3d46 zK$JFV%ORhVY@uq?Vl&Z9eJCLG>_P))s-3TWyeE`sRkPRhrMqFfleqF@cT!>CQ2s6l}^QomY&mC zX!2KN&FqWnqYniBdRL@S>pd=Gi1{wUYpLffvuaMN&VlztcGF?&oFO1M??MU%%?ZVn zl@o>n$lOB7ECYk0nXVReLZ z%=PwmafM_muXqs~-MfoP($DDVJw;1iEgn+MzO{K~QmL`|XxL(QN^WEIC_#{?mQOj`Td3XO{-*$49Ba8`a$Y zjd4bOJzGdfNETpI2ge=T8OqBj3G(wltT7!GuxCHS`*)D}^IgCNel%YuG2iylj*P@G zeSPv-%>2=wX9MiS@v&_eiGlQ=C$CJb%Nk0y!+e~1FWB=;en2-Hvim*z<1UMk@-Y}hRkM~%= zwm}*gb1aXKA>ecxDOAj7%EA!EFzD*(!4UP-MXr1)ETg76mMw}>+0{J^PCc`rvM40$ z8Qi}^%b!6ixB!le=-XJPHyR@oF0uoUDZ8Tne1}?MxmN<1u7Vz7jg~VEtE_9hK#k1d z(EC$29z}y)q>DL^=i8NHxjy8DDwN*%f{^s@Vi;^2Aa9X6IzLoHdCiIuy;8h z^vI*Y?Q!U=!uj2zbVRNI=0TnPrVI&?RZJ~P*6n!gDF>ERf2f3vwYWPIifzwS<%@ZG zc!X`p$jD4Wwj0cG<^TCx|LfWp_qA?6HZK@Xc_`9x|H=T<(R|?u@@t+)ndCr;TmEt# z@Wkw5V~$pKBW*lHB{w~3gXP^2`6UL@O5&aVDofgD;8LdFO2NrFmCnCzkedJ&^V3#< z!w~SmXukgKdkNS+kcP~ZV^UK~$Oi%j2c?7^I-ufiuFeOfCnO#TzUyV}jcoR8=^YqO zZuVeL*l1C5EyXe7rIlb$l**LGljE0ph_$w%*owK%mB>!JW0!k|Ur04!WMZi3bXh}D za=(79>&H739@MESVZ7QnB2jN|ZwYbn(Xjo41Jt%wW{bby=YO8YKi7s(tYLeZs1wn* zfqyt~%eX-0MI4GE&t9tClGJ*!fp_oqFsilOIZeQ8g%w?r;T0Hw-&w0kyIsx&uH(8 zCf;5H%}f_FNQ|#}d3lFUpFjVeUG&iJPbB|mpKxFJ38Lvvak)_=P-U?}s5|t3efjo0 z=pC^Re6m<^Ej1X*ulSghQ?RhlXjvI(99Tp1bjFWVC=IJvk&AE5-lVjMJl2d>hK`DXI>%G z{sE+Ei)jg(+S#56#Jl7IIYeFL7!WDFfoO|0?oM4|ApoL9T?j) z;S4u*^3&{{#wOiqXGxlg|Ttz7$0;25xQ2G`V^0Hpy8{3gEXnGVo(qbLVWt%Z& z$JYIcLlXxqji#JFfBxF@MIUtw?!S=FU$h-t=+bo@#WMCLCV{vIHqNs=MV$U*$j)`; zFMlyt%Xxg&35Dcs+<{TQK-3XfC0JNAw9%Q&itdMrom;&%+Hc9rbpM=uO5juGT{VMb zRekJ${5v!ckI^^`HRB0B+ep@J1Y{djae2=ejpekp(}V}i=RXw^ap?UJ=M&2io?=%d zbz12Js4A75-sc#S6%cwcvf_X~UTuuP^Un+Oe=oo(syqrQJl|C(l*owWv~ouiz)be* zS^gCa!NAKr6;*g6At6BoSQTe6TTG4snf&^Wo#HPe0Q1o*=9)PDo1iGWt8CyK@c{ah z;7e}uaG5Gv+0dSgI#Wrc_LRH%fbGk z;DYrwAy4AW5xlp^D^qyGTw_oRxNuZnpzdE7!D3-ynsIJP=;`Tot&&85xH?sn_5XVs zz~h22im+jA`--*%l|&3Z%Q679n}_v5)!&eb4<8anL5%<=Gpp0WF#xRL<@-Sk2a#Ui z!0Q$v%fiIYce!#+oO=;|e+T5A`vMjiu*&ErO_WhPFwO=3B?2A{`dvZl>u#bO6?&bI z)xo~BiiuAl0!X8xQq1 zua+PWIyYc8>uBF_u@ym&IZhIh6rt_UiuLGJ;4A?W}!o12x{FQ-z-RFNqu{Y`M6DdSHtj??z zFNKj$uenT*j~9Yq$p3nG|9XeMO1E$OlkLpKCnn~@DX6A&9C%P*{9I-dLCe;2!R6W`m!;3zN!5i2QO?7|gTk@pm80>)drvx1 zpy6X`qPlZ?81p?Ru-vO7Jrk~$(ANCCPeH!FjFaq8J#9uDm~#Y6+8q19uOl;p`NI7dMTz5-EzzJvQUDcbp=>%2 z1PIb4bPi2%%CCU6*Z0kWhLJ`ajeEFrhpRZbfu^7i=3(E1X>0MY%I&#R2e&)&Dn_Y~ z48m{1ZXKwvc8o)hf%Hor%D{2MWW__MZ43QIFLVL&-=5*S3)G`aPydYDOn0H zQYSY;`jM&S=3`9kphHvh3F$8VzvqZ4@A>*`1aRqWB1MA6Ube^HdX!T7lgZd8R-pX~ z%5<{rMxgX32J7s)uoayS7pcd97vXl3s-`t2MrM$LL^ruK7{`pP?vCF3;W$B;<{H6& z7SAGP17EY5n6H5^5k2M;mmY9=2*`~ETF_u7Dyrgs1NNN&9giO{Etfk*#)&5(G0+&^xo$i_i(Q~dva3VfBy;@&c;iql>4z=KYcN0-#Tl^>W{S^i%#9x*b_-kuN z^DXVy3$R+xzod<9bntG2b9?*7fkafm`Pa920Q_#m68y;?ISRzli3e4J_-(P={q%SI z{to?8h~7T5va-_uWk5GMFe}Ktr-piYwSHX0Qi1)K0h%_JPS|ts7vo>c0SHk1N3g^8 zo2=JE5<|r&sz)%3i;Iv71JNC@?hG}xyePGuZxiby@GKssENd39m@JM)GgWYEVO-6W zN6j!TU^CY1=`ZOJkN{2PYn*A|`2^y`cwTs))0=-yAbhehdiyE!w95_^78^@7@{w!I z*CZ{{4_3D%doF(+<92fRoJ~ymwg&Ynu5iC*(*;H?U9@iULpBzeUl~~)nB1AV#%btN zl3$%4;zS3K|M>A^{j4shf`6P&ACW?Ds3Ui}oC+zQPgSl0`o*Qs10!*4nV4a1not#? zery?qucRu;GMbtxsA>0(er%Pe7%K2!Lje{MCieQapj09oA)nboU1}@55iDuB)r^in z)O~u6rS!bq&ff-gA?wV0i9ZkI{=(J}%4|j$a+^NKEcxX0JmX#2=@=yQPmK!n@^#HE z>BuC;isF4p^xGd~kW6oL*p80vs+Nf8Fn&i;zHn3@)Lh(YU}oq~whsCT;vRp0fAcWY zdI%`GI{-ITkWdajf8;e1Ft&sX5b{^hefLsObd9ea2F@_oP(fXR0vK4z10zF1mWGbi zSbG&RR>mF*XnlSCpc4Mx=m65spFb-{#b;Nf(F5ZY`FkeiV}ap=+TDMij`G$>J|rcL z%e7M7+Ve#6Rku$78s&G#Q9yeruAOfv!Zkp7voA2@gt2iv8II#BbsZ}OmE}C{6J1e|< zttVntP>>2eMw>eta=|;Y$lMghf{KePVblEMF%kg>qyXvOa_c{zE|bVT%MTS zBgH{fQY+V>r?q5gYRV53$w4Rbr^xIgieD~w!+eS>bdYwy@)7TypHCX!TRnQf4p0y| zQX*tQgTf$Wq7RY|K`bF}oAw&;eH7lEHvvcwPGLF~1yP9&tdbSU0evHj)<>Q4HexY& zOi)_oJ?$A|IRKqn9^9ItA)>FivbnNak{Qtu(Sgap^6oBW7s=@8D3MVn@a)tZ4bIQc zr_Of@sko@xh!%aG8IH$_yHDDv_(VE|{bN$Cg)!tk!X{Bvs#kcc`za+yxpn&JmoMRX zqM!|`mFNDqomG1@oZ%X_VzSyClO*UhWJEVkCjap|{`1-Dx`SXco%S}~SWH4fp^MyP zI4u{LDN)cfG6G9L<#i%w6z6y`Vf1_^x*%oMoK?InN@3n3G&8eSisR*^B+{tlc)