@streamr/trackerless-network 100.2.4-beta.0 → 100.2.5-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/package.json +6 -7
  2. package/dist/src/NetworkStack.js +1 -1
  3. package/dist/src/NetworkStack.js.map +1 -1
  4. package/dist/src/logic/ContentDeliveryLayerNode.js +9 -8
  5. package/dist/src/logic/ContentDeliveryLayerNode.js.map +1 -1
  6. package/dist/src/logic/ContentDeliveryManager.d.ts +2 -0
  7. package/dist/src/logic/ContentDeliveryManager.js +43 -13
  8. package/dist/src/logic/ContentDeliveryManager.js.map +1 -1
  9. package/dist/src/logic/EntryPointDiscovery.d.ts +3 -14
  10. package/dist/src/logic/EntryPointDiscovery.js +9 -83
  11. package/dist/src/logic/EntryPointDiscovery.js.map +1 -1
  12. package/dist/src/logic/Layer0Node.d.ts +2 -2
  13. package/dist/src/logic/Layer1Node.d.ts +7 -3
  14. package/dist/src/logic/StreamPartNetworkSplitAvoidance.d.ts +18 -0
  15. package/dist/src/logic/StreamPartNetworkSplitAvoidance.js +73 -0
  16. package/dist/src/logic/StreamPartNetworkSplitAvoidance.js.map +1 -0
  17. package/dist/src/logic/StreamPartReconnect.d.ts +11 -0
  18. package/dist/src/logic/StreamPartReconnect.js +35 -0
  19. package/dist/src/logic/StreamPartReconnect.js.map +1 -0
  20. package/dist/src/proto/google/protobuf/any.d.ts +4 -11
  21. package/dist/src/proto/google/protobuf/any.js.map +1 -1
  22. package/dist/src/proto/google/protobuf/empty.d.ts +1 -0
  23. package/dist/src/proto/google/protobuf/empty.js.map +1 -1
  24. package/dist/src/proto/google/protobuf/timestamp.d.ts +3 -9
  25. package/dist/src/proto/google/protobuf/timestamp.js.map +1 -1
  26. package/dist/src/proto/packages/dht/protos/DhtRpc.d.ts +2 -2
  27. package/dist/src/proto/packages/dht/protos/DhtRpc.js +1 -1
  28. package/dist/src/proto/packages/dht/protos/DhtRpc.js.map +1 -1
  29. package/dist/test/benchmark/first-message.js +1 -1
  30. package/dist/test/benchmark/first-message.js.map +1 -1
  31. package/dist/test/utils/utils.js +2 -0
  32. package/dist/test/utils/utils.js.map +1 -1
  33. package/package.json +6 -7
  34. package/src/NetworkStack.ts +1 -1
  35. package/src/logic/ContentDeliveryLayerNode.ts +11 -9
  36. package/src/logic/ContentDeliveryManager.ts +44 -16
  37. package/src/logic/EntryPointDiscovery.ts +14 -102
  38. package/src/logic/Layer0Node.ts +2 -2
  39. package/src/logic/Layer1Node.ts +7 -3
  40. package/src/logic/StreamPartNetworkSplitAvoidance.ts +89 -0
  41. package/src/logic/StreamPartReconnect.ts +37 -0
  42. package/src/proto/google/protobuf/any.ts +4 -11
  43. package/src/proto/google/protobuf/empty.ts +1 -0
  44. package/src/proto/google/protobuf/timestamp.ts +3 -9
  45. package/src/proto/packages/dht/protos/DhtRpc.ts +3 -3
  46. package/test/benchmark/first-message.ts +1 -1
  47. package/test/integration/ContentDeliveryLayerNode-Layer1Node-Latencies.test.ts +2 -0
  48. package/test/integration/ContentDeliveryLayerNode-Layer1Node.test.ts +2 -0
  49. package/test/integration/ContentDeliveryManager.test.ts +2 -0
  50. package/test/integration/Inspect.test.ts +2 -1
  51. package/test/integration/NetworkNode.test.ts +4 -2
  52. package/test/integration/NodeInfoRpc.test.ts +2 -0
  53. package/test/integration/joining-streams-on-offline-peers.test.ts +3 -0
  54. package/test/integration/stream-without-default-entrypoints.test.ts +2 -0
  55. package/test/integration/streamEntryPointReplacing.test.ts +2 -0
  56. package/test/unit/ContentDeliveryLayerNode.test.ts +5 -5
  57. package/test/unit/ContentDeliveryManager.test.ts +1 -1
  58. package/test/unit/ContentDeliveryRpcLocal.test.ts +1 -1
  59. package/test/unit/EntrypointDiscovery.test.ts +9 -40
  60. package/test/unit/Inspector.test.ts +1 -1
  61. package/test/unit/NeighborUpdateRpcLocal.test.ts +1 -1
  62. package/test/unit/NodeList.test.ts +1 -1
  63. package/test/unit/StreamPartNetworkSplitAvoidance.test.ts +32 -0
  64. package/test/unit/StreamPartReconnect.test.ts +30 -0
  65. package/test/unit/TemporaryConnectionRpcLocal.test.ts +1 -1
  66. package/test/utils/fake/FakeEntryPointDiscovery.ts +29 -0
  67. package/test/utils/mock/MockConnectionsView.ts +18 -0
  68. package/test/utils/mock/MockLayer0Node.ts +5 -14
  69. package/test/utils/mock/MockLayer1Node.ts +2 -2
  70. package/test/utils/mock/{Transport.ts → MockTransport.ts} +2 -16
  71. package/test/utils/utils.ts +2 -0
@@ -19,12 +19,14 @@ import {
19
19
  import { EventEmitter } from 'eventemitter3'
20
20
  import { sampleSize } from 'lodash'
21
21
  import { ProxyDirection, StreamMessage, StreamPartitionInfo } from '../proto/packages/trackerless-network/protos/NetworkRpc'
22
- import { EntryPointDiscovery, NETWORK_SPLIT_AVOIDANCE_LIMIT } from './EntryPointDiscovery'
22
+ import { ENTRYPOINT_STORE_LIMIT, EntryPointDiscovery } from './EntryPointDiscovery'
23
23
  import { Layer0Node } from './Layer0Node'
24
24
  import { Layer1Node } from './Layer1Node'
25
25
  import { ContentDeliveryLayerNode } from './ContentDeliveryLayerNode'
26
26
  import { createContentDeliveryLayerNode } from './createContentDeliveryLayerNode'
27
27
  import { ProxyClient } from './proxy/ProxyClient'
28
+ import { StreamPartReconnect } from './StreamPartReconnect'
29
+ import { MIN_NEIGHBOR_COUNT as NETWORK_SPLIT_AVOIDANCE_MIN_NEIGHBOR_COUNT, StreamPartNetworkSplitAvoidance } from './StreamPartNetworkSplitAvoidance'
28
30
 
29
31
  export type StreamPartDelivery = {
30
32
  broadcast: (msg: StreamMessage) => void
@@ -34,6 +36,7 @@ export type StreamPartDelivery = {
34
36
  layer1Node: Layer1Node
35
37
  node: ContentDeliveryLayerNode
36
38
  entryPointDiscovery: EntryPointDiscovery
39
+ networkSplitAvoidance: StreamPartNetworkSplitAvoidance
37
40
  } | {
38
41
  proxied: true
39
42
  client: ProxyClient
@@ -136,23 +139,30 @@ export class ContentDeliveryManager extends EventEmitter<Events> {
136
139
  const entryPointDiscovery = new EntryPointDiscovery({
137
140
  streamPartId,
138
141
  localPeerDescriptor: this.getPeerDescriptor(),
139
- layer1Node,
140
142
  fetchEntryPointData: (key) => this.layer0Node!.fetchDataFromDht(key),
141
143
  storeEntryPointData: (key, data) => this.layer0Node!.storeDataToDht(key, data),
142
144
  deleteEntryPointData: async (key) => this.layer0Node!.deleteDataFromDht(key, false)
143
145
  })
146
+ const networkSplitAvoidance = new StreamPartNetworkSplitAvoidance({
147
+ layer1Node,
148
+ discoverEntryPoints: async () => entryPointDiscovery.discoverEntryPoints()
149
+ })
144
150
  const node = this.createContentDeliveryLayerNode(
145
151
  streamPartId,
146
152
  layer1Node,
147
153
  () => entryPointDiscovery.isLocalNodeEntryPoint()
148
154
  )
155
+ const streamPartReconnect = new StreamPartReconnect(layer1Node, entryPointDiscovery)
149
156
  streamPart = {
150
157
  proxied: false,
151
158
  layer1Node,
152
159
  node,
153
160
  entryPointDiscovery,
161
+ networkSplitAvoidance,
154
162
  broadcast: (msg: StreamMessage) => node.broadcast(msg),
155
163
  stop: async () => {
164
+ streamPartReconnect.destroy()
165
+ networkSplitAvoidance.destroy()
156
166
  await entryPointDiscovery.destroy()
157
167
  node.stop()
158
168
  await layer1Node.stop()
@@ -166,9 +176,17 @@ export class ContentDeliveryManager extends EventEmitter<Events> {
166
176
  if (this.destroyed || entryPointDiscovery.isLocalNodeEntryPoint() || this.knownStreamPartEntryPoints.has(streamPartId)) {
167
177
  return
168
178
  }
169
- const entryPoints = await entryPointDiscovery.discoverEntryPointsFromDht(0)
170
- await entryPointDiscovery.storeSelfAsEntryPointIfNecessary(entryPoints.discoveredEntryPoints.length)
179
+ const entryPoints = await entryPointDiscovery.discoverEntryPoints()
180
+ if (entryPoints.length < ENTRYPOINT_STORE_LIMIT) {
181
+ await entryPointDiscovery.storeAndKeepLocalNodeAsEntryPoint()
182
+ }
171
183
  }
184
+ layer1Node.on('manualRejoinRequired', async () => {
185
+ if (!streamPartReconnect.isRunning() && !networkSplitAvoidance.isRunning()) {
186
+ logger.debug('Manual rejoin required for stream part', { streamPartId })
187
+ await streamPartReconnect.reconnect()
188
+ }
189
+ })
172
190
  node.on('entryPointLeaveDetected', () => handleEntryPointLeave())
173
191
  setImmediate(async () => {
174
192
  try {
@@ -188,29 +206,39 @@ export class ContentDeliveryManager extends EventEmitter<Events> {
188
206
  }
189
207
  await streamPart.layer1Node.start()
190
208
  await streamPart.node.start()
191
- let entryPoints = this.knownStreamPartEntryPoints.get(streamPartId) ?? []
192
- const discoveryResult = await entryPointDiscovery.discoverEntryPointsFromDht(
193
- entryPoints.length
194
- )
195
- entryPoints = entryPoints.concat(discoveryResult.discoveredEntryPoints)
196
- await Promise.all([
197
- streamPart.layer1Node.joinDht(sampleSize(entryPoints, NETWORK_SPLIT_AVOIDANCE_LIMIT)),
198
- streamPart.layer1Node.joinRing()
199
- ])
200
- if (discoveryResult.entryPointsFromDht) {
201
- await entryPointDiscovery.storeSelfAsEntryPointIfNecessary(entryPoints.length)
209
+ const knownEntryPoints = this.knownStreamPartEntryPoints.get(streamPartId)
210
+ if (knownEntryPoints !== undefined) {
211
+ await Promise.all([
212
+ streamPart.layer1Node.joinDht(knownEntryPoints),
213
+ streamPart.layer1Node.joinRing()
214
+ ])
215
+ } else {
216
+ const entryPoints = await entryPointDiscovery.discoverEntryPoints()
217
+ await Promise.all([
218
+ streamPart.layer1Node.joinDht(sampleSize(entryPoints, NETWORK_SPLIT_AVOIDANCE_MIN_NEIGHBOR_COUNT)),
219
+ streamPart.layer1Node.joinRing()
220
+ ])
221
+ if (entryPoints.length < ENTRYPOINT_STORE_LIMIT) {
222
+ await entryPointDiscovery.storeAndKeepLocalNodeAsEntryPoint()
223
+ if (streamPart.layer1Node.getNeighborCount() < NETWORK_SPLIT_AVOIDANCE_MIN_NEIGHBOR_COUNT) {
224
+ setImmediate(() => streamPart.networkSplitAvoidance.avoidNetworkSplit())
225
+ }
226
+ }
202
227
  }
203
228
  }
204
229
 
205
230
  private createLayer1Node(streamPartId: StreamPartID, entryPoints: PeerDescriptor[]): Layer1Node {
206
231
  return new DhtNode({
207
232
  transport: this.layer0Node!,
233
+ connectionsView: this.layer0Node!.getConnectionsView(),
208
234
  serviceId: 'layer1::' + streamPartId,
209
235
  peerDescriptor: this.layer0Node!.getLocalPeerDescriptor(),
210
236
  entryPoints,
211
237
  numberOfNodesPerKBucket: 4, // TODO use config option or named constant?
212
238
  rpcRequestTimeout: EXISTING_CONNECTION_TIMEOUT,
213
- dhtJoinTimeout: 20000 // TODO use config option or named constant?
239
+ dhtJoinTimeout: 20000, // TODO use config option or named constant?
240
+ periodicallyPingNeighbors: true,
241
+ periodicallyPingRingContacts: true
214
242
  })
215
243
  }
216
244
 
@@ -3,14 +3,12 @@ import {
3
3
  DhtAddress,
4
4
  PeerDescriptor,
5
5
  areEqualPeerDescriptors,
6
- getDhtAddressFromRaw,
7
- getNodeIdFromPeerDescriptor
6
+ getDhtAddressFromRaw
8
7
  } from '@streamr/dht'
9
8
  import { StreamPartID } from '@streamr/protocol'
10
- import { Logger, scheduleAtInterval, wait } from '@streamr/utils'
9
+ import { Logger, scheduleAtInterval } from '@streamr/utils'
11
10
  import { createHash } from 'crypto'
12
11
  import { Any } from '../proto/google/protobuf/any'
13
- import { Layer1Node } from './Layer1Node'
14
12
 
15
13
  export const streamPartIdToDataKey = (streamPartId: StreamPartID): DhtAddress => {
16
14
  return getDhtAddressFromRaw(new Uint8Array((createHash('sha1').update(streamPartId).digest())))
@@ -20,46 +18,13 @@ const parseEntryPointData = (dataEntries: DataEntry[]): PeerDescriptor[] => {
20
18
  return dataEntries.filter((entry) => !entry.deleted).map((entry) => Any.unpack(entry.data!, PeerDescriptor))
21
19
  }
22
20
 
23
- interface FindEntryPointsResult {
24
- entryPointsFromDht: boolean
25
- discoveredEntryPoints: PeerDescriptor[]
26
- }
27
-
28
- const exponentialRunOff = async (
29
- task: () => Promise<void>,
30
- description: string,
31
- abortSignal: AbortSignal,
32
- baseDelay = 500,
33
- maxAttempts = 6
34
- ): Promise<void> => {
35
- for (let i = 1; i <= maxAttempts; i++) {
36
- if (abortSignal.aborted) {
37
- return
38
- }
39
- const factor = 2 ** i
40
- const delay = baseDelay * factor
41
- try {
42
- await task()
43
- } catch (e: any) {
44
- logger.trace(`${description} failed, retrying in ${delay} ms`)
45
- }
46
- try { // Abort controller throws unexpected errors in destroy?
47
- await wait(delay, abortSignal)
48
- } catch (err) {
49
- logger.trace(`${err}`) // TODO Do we need logging?
50
- }
51
- }
52
- }
53
-
54
21
  const logger = new Logger(module)
55
22
 
56
23
  export const ENTRYPOINT_STORE_LIMIT = 8
57
- export const NETWORK_SPLIT_AVOIDANCE_LIMIT = 4
58
24
 
59
25
  interface EntryPointDiscoveryConfig {
60
26
  streamPartId: StreamPartID
61
27
  localPeerDescriptor: PeerDescriptor
62
- layer1Node: Layer1Node
63
28
  fetchEntryPointData: (key: DhtAddress) => Promise<DataEntry[]>
64
29
  storeEntryPointData: (key: DhtAddress, data: Any) => Promise<PeerDescriptor[]>
65
30
  deleteEntryPointData: (key: DhtAddress) => Promise<void>
@@ -67,74 +32,39 @@ interface EntryPointDiscoveryConfig {
67
32
  }
68
33
 
69
34
  export class EntryPointDiscovery {
35
+
70
36
  private readonly abortController: AbortController
71
37
  private readonly config: EntryPointDiscoveryConfig
72
38
  private readonly storeInterval: number
73
- private readonly networkSplitAvoidedNodes: Set<DhtAddress> = new Set()
74
39
  private isLocalNodeStoredAsEntryPoint = false
40
+
75
41
  constructor(config: EntryPointDiscoveryConfig) {
76
42
  this.config = config
77
43
  this.abortController = new AbortController()
78
44
  this.storeInterval = this.config.storeInterval ?? 60000
79
45
  }
80
46
 
81
- async discoverEntryPointsFromDht(knownEntryPointCount: number): Promise<FindEntryPointsResult> {
82
- if (knownEntryPointCount > 0) {
83
- return {
84
- entryPointsFromDht: false,
85
- discoveredEntryPoints: []
86
- }
87
- }
88
- const discoveredEntryPoints = await this.discoverEntryPoints()
89
- if (discoveredEntryPoints.length === 0) {
90
- discoveredEntryPoints.push(this.config.localPeerDescriptor)
91
- }
92
- return {
93
- discoveredEntryPoints,
94
- entryPointsFromDht: true
95
- }
96
- }
97
-
98
- private async discoverEntryPoints(): Promise<PeerDescriptor[]> {
47
+ async discoverEntryPoints(): Promise<PeerDescriptor[]> {
99
48
  const dataKey = streamPartIdToDataKey(this.config.streamPartId)
100
- const discoveredEntryPoints = await this.queryEntrypoints(dataKey)
101
- const filtered = discoveredEntryPoints.filter((node) =>
102
- !this.networkSplitAvoidedNodes.has(getNodeIdFromPeerDescriptor(node)))
103
- // If all discovered entry points have previously been detected as offline, try again
104
- if (filtered.length > 0) {
105
- return filtered
106
- } else {
107
- return discoveredEntryPoints
108
- }
109
- }
110
-
111
- private async queryEntrypoints(key: DhtAddress): Promise<PeerDescriptor[]> {
112
- logger.trace(`Finding data from dht node ${getNodeIdFromPeerDescriptor(this.config.localPeerDescriptor)}`)
49
+ logger.trace(`Discovering entry points for key ${dataKey}`)
113
50
  try {
114
- const result = await this.config.fetchEntryPointData(key)
51
+ const result = await this.config.fetchEntryPointData(dataKey)
115
52
  return parseEntryPointData(result)
116
53
  } catch (err) {
117
54
  return []
118
- }
55
+ }
119
56
  }
120
57
 
121
- async storeSelfAsEntryPointIfNecessary(currentEntrypointCount: number): Promise<void> {
58
+ async storeAndKeepLocalNodeAsEntryPoint(): Promise<void> {
122
59
  if (this.abortController.signal.aborted) {
123
60
  return
124
61
  }
125
- const possibleNetworkSplitDetected = this.config.layer1Node.getNeighborCount() < NETWORK_SPLIT_AVOIDANCE_LIMIT
126
- if ((currentEntrypointCount < ENTRYPOINT_STORE_LIMIT) || possibleNetworkSplitDetected) {
127
- this.isLocalNodeStoredAsEntryPoint = true
128
- await this.storeSelfAsEntryPoint()
129
- await this.keepSelfAsEntryPoint()
130
- }
131
- if (possibleNetworkSplitDetected) {
132
- // TODO should we catch possible promise rejection?
133
- setImmediate(() => this.avoidNetworkSplit())
134
- }
62
+ this.isLocalNodeStoredAsEntryPoint = true
63
+ await this.storeLocalNodeAsEntryPoint()
64
+ await this.keepSelfAsEntryPoint()
135
65
  }
136
66
 
137
- private async storeSelfAsEntryPoint(): Promise<void> {
67
+ private async storeLocalNodeAsEntryPoint(): Promise<void> {
138
68
  const localPeerDescriptor = this.config.localPeerDescriptor
139
69
  const dataToStore = Any.pack(localPeerDescriptor, PeerDescriptor)
140
70
  try {
@@ -151,7 +81,7 @@ export class EntryPointDiscovery {
151
81
  const discovered = await this.discoverEntryPoints()
152
82
  if (discovered.length < ENTRYPOINT_STORE_LIMIT
153
83
  || discovered.some((peerDescriptor) => areEqualPeerDescriptors(peerDescriptor, this.config.localPeerDescriptor))) {
154
- await this.storeSelfAsEntryPoint()
84
+ await this.storeLocalNodeAsEntryPoint()
155
85
  }
156
86
  } catch (err) {
157
87
  logger.debug(`Failed to keep self as entrypoint for ${this.config.streamPartId}`)
@@ -159,24 +89,6 @@ export class EntryPointDiscovery {
159
89
  }, this.storeInterval, false, this.abortController.signal)
160
90
  }
161
91
 
162
- private async avoidNetworkSplit(): Promise<void> {
163
- await exponentialRunOff(async () => {
164
- const rediscoveredEntrypoints = await this.discoverEntryPoints()
165
- await this.config.layer1Node.joinDht(rediscoveredEntrypoints, false, false)
166
- if (this.config.layer1Node.getNeighborCount() < NETWORK_SPLIT_AVOIDANCE_LIMIT) {
167
- // Filter out nodes that are not neighbors as those nodes are assumed to be offline
168
- const nodesToAvoid = rediscoveredEntrypoints
169
- .filter((peer) => !this.config.layer1Node.getNeighbors()
170
- .some((neighbor) => areEqualPeerDescriptors(neighbor, peer)))
171
- .map((peer) => getNodeIdFromPeerDescriptor(peer))
172
- nodesToAvoid.forEach((node) => this.networkSplitAvoidedNodes.add(node))
173
- throw new Error(`Network split is still possible`)
174
- }
175
- }, 'avoid network split', this.abortController.signal)
176
- this.networkSplitAvoidedNodes.clear()
177
- logger.trace(`Network split avoided`)
178
- }
179
-
180
92
  public isLocalNodeEntryPoint(): boolean {
181
93
  return this.isLocalNodeStoredAsEntryPoint
182
94
  }
@@ -1,4 +1,4 @@
1
- import { DataEntry, DhtAddress, ITransport, PeerDescriptor } from '@streamr/dht'
1
+ import { ConnectionsView, DataEntry, DhtAddress, ITransport, PeerDescriptor } from '@streamr/dht'
2
2
  import { Any } from '../proto/google/protobuf/any'
3
3
 
4
4
  export interface Layer0Node extends ITransport {
@@ -11,7 +11,7 @@ export interface Layer0Node extends ITransport {
11
11
  waitForNetworkConnectivity(): Promise<void>
12
12
  getTransport(): ITransport
13
13
  getNeighbors(): PeerDescriptor[]
14
- getConnections(): PeerDescriptor[]
14
+ getConnectionsView(): ConnectionsView
15
15
  start(): Promise<void>
16
16
  stop(): Promise<void>
17
17
  }
@@ -1,8 +1,9 @@
1
1
  import { DhtAddress, PeerDescriptor, RingContacts } from '@streamr/dht'
2
2
 
3
3
  export interface Layer1NodeEvents {
4
- closestContactAdded: (peerDescriptor: PeerDescriptor) => void
5
- closestContactRemoved: (peerDescriptor: PeerDescriptor) => void
4
+ manualRejoinRequired: () => void
5
+ nearbyContactAdded: (peerDescriptor: PeerDescriptor) => void
6
+ nearbyContactRemoved: (peerDescriptor: PeerDescriptor) => void
6
7
  randomContactAdded: (peerDescriptor: PeerDescriptor) => void
7
8
  randomContactRemoved: (peerDescriptor: PeerDescriptor) => void
8
9
  ringContactAdded: (peerDescriptor: PeerDescriptor) => void
@@ -13,9 +14,12 @@ export interface Layer1Node {
13
14
  on<T extends keyof Layer1NodeEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor) => void): void
14
15
  once<T extends keyof Layer1NodeEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor) => void): void
15
16
  off<T extends keyof Layer1NodeEvents>(eventName: T, listener: (peerDescriptor: PeerDescriptor) => void): void
17
+ on<T extends keyof Layer1NodeEvents>(eventName: T, listener: () => void): void
18
+ once<T extends keyof Layer1NodeEvents>(eventName: T, listener: () => void): void
19
+ off<T extends keyof Layer1NodeEvents>(eventName: T, listener: () => void): void
16
20
  removeContact: (nodeId: DhtAddress) => void
17
21
  getClosestContacts: (maxCount?: number) => PeerDescriptor[]
18
- getRandomContacts: () => PeerDescriptor[]
22
+ getRandomContacts: (maxCount?: number) => PeerDescriptor[]
19
23
  getRingContacts: () => RingContacts
20
24
  getNeighbors: () => PeerDescriptor[]
21
25
  getNeighborCount(): number
@@ -0,0 +1,89 @@
1
+ import { areEqualPeerDescriptors, DhtAddress, getNodeIdFromPeerDescriptor, PeerDescriptor } from '@streamr/dht'
2
+ import { Logger, wait } from '@streamr/utils'
3
+ import { Layer1Node } from './Layer1Node'
4
+
5
+ /*
6
+ * Tries to find new neighbors if we currently have less than MIN_NEIGHBOR_COUNT neigbors. It does so by
7
+ * rejoining the stream's control layer network.
8
+ *
9
+ * This way we can avoid some network split scenarios. The functionality is most relevant for small stream
10
+ * networks.
11
+ */
12
+
13
+ const logger = new Logger(module)
14
+
15
+ const exponentialRunOff = async (
16
+ task: () => Promise<void>,
17
+ description: string,
18
+ abortSignal: AbortSignal,
19
+ baseDelay = 500,
20
+ maxAttempts = 6
21
+ ): Promise<void> => {
22
+ for (let i = 1; i <= maxAttempts; i++) {
23
+ if (abortSignal.aborted) {
24
+ return
25
+ }
26
+ const factor = 2 ** i
27
+ const delay = baseDelay * factor
28
+ try {
29
+ await task()
30
+ } catch (e: any) {
31
+ logger.debug(`${description} failed, retrying in ${delay} ms`)
32
+ }
33
+ try { // Abort controller throws unexpected errors in destroy?
34
+ await wait(delay, abortSignal)
35
+ } catch (err) {
36
+ logger.trace(`${err}`) // TODO Do we need logging?
37
+ }
38
+ }
39
+ }
40
+
41
+ export const MIN_NEIGHBOR_COUNT = 4
42
+
43
+ export interface StreamPartNetworkSplitAvoidanceConfig {
44
+ layer1Node: Layer1Node
45
+ discoverEntryPoints: (excludedNodes?: Set<DhtAddress>) => Promise<PeerDescriptor[]>
46
+ exponentialRunOfBaseDelay?: number
47
+ }
48
+
49
+ export class StreamPartNetworkSplitAvoidance {
50
+
51
+ private readonly abortController: AbortController
52
+ private readonly config: StreamPartNetworkSplitAvoidanceConfig
53
+ private readonly excludedNodes: Set<DhtAddress> = new Set()
54
+ private running = false
55
+
56
+ constructor(config: StreamPartNetworkSplitAvoidanceConfig) {
57
+ this.config = config
58
+ this.abortController = new AbortController()
59
+ }
60
+
61
+ public async avoidNetworkSplit(): Promise<void> {
62
+ this.running = true
63
+ await exponentialRunOff(async () => {
64
+ const discoveredEntrypoints = await this.config.discoverEntryPoints()
65
+ const filteredEntryPoints = discoveredEntrypoints.filter((peer) => !this.excludedNodes.has(getNodeIdFromPeerDescriptor(peer)))
66
+ await this.config.layer1Node.joinDht(filteredEntryPoints, false, false)
67
+ if (this.config.layer1Node.getNeighborCount() < MIN_NEIGHBOR_COUNT) {
68
+ // Filter out nodes that are not neighbors as those nodes are assumed to be offline
69
+ const newExcludes = filteredEntryPoints
70
+ .filter((peer) => !this.config.layer1Node.getNeighbors()
71
+ .some((neighbor) => areEqualPeerDescriptors(neighbor, peer)))
72
+ .map((peer) => getNodeIdFromPeerDescriptor(peer))
73
+ newExcludes.forEach((node) => this.excludedNodes.add(node))
74
+ throw new Error(`Network split is still possible`)
75
+ }
76
+ }, 'avoid network split', this.abortController.signal, this.config.exponentialRunOfBaseDelay)
77
+ this.running = false
78
+ this.excludedNodes.clear()
79
+ logger.trace(`Network split avoided`)
80
+ }
81
+
82
+ public isRunning(): boolean {
83
+ return this.running
84
+ }
85
+
86
+ destroy(): void {
87
+ this.abortController.abort()
88
+ }
89
+ }
@@ -0,0 +1,37 @@
1
+ import { scheduleAtInterval } from '@streamr/utils'
2
+ import { EntryPointDiscovery } from './EntryPointDiscovery'
3
+ import { Layer1Node } from './Layer1Node'
4
+
5
+ const DEFAULT_RECONNECT_INTERVAL = 30 * 1000
6
+ export class StreamPartReconnect {
7
+ private abortController?: AbortController
8
+ private readonly layer1Node: Layer1Node
9
+ private readonly entryPointDiscovery: EntryPointDiscovery
10
+
11
+ constructor(layer1Node: Layer1Node, entryPointDiscovery: EntryPointDiscovery) {
12
+ this.layer1Node = layer1Node
13
+ this.entryPointDiscovery = entryPointDiscovery
14
+ }
15
+
16
+ async reconnect(timeout = DEFAULT_RECONNECT_INTERVAL): Promise<void> {
17
+ this.abortController = new AbortController()
18
+ await scheduleAtInterval(async () => {
19
+ const entryPoints = await this.entryPointDiscovery.discoverEntryPoints()
20
+ await this.layer1Node.joinDht(entryPoints)
21
+ if (this.entryPointDiscovery.isLocalNodeEntryPoint()) {
22
+ await this.entryPointDiscovery.storeAndKeepLocalNodeAsEntryPoint()
23
+ }
24
+ if (this.layer1Node.getNeighborCount() > 0) {
25
+ this.abortController!.abort()
26
+ }
27
+ }, timeout, true, this.abortController.signal)
28
+ }
29
+
30
+ isRunning(): boolean {
31
+ return this.abortController ? !this.abortController.signal.aborted : false
32
+ }
33
+
34
+ destroy(): void {
35
+ this.abortController?.abort()
36
+ }
37
+ }
@@ -73,10 +73,6 @@ import { MessageType } from "@protobuf-ts/runtime";
73
73
  * if (any.is(Foo.class)) {
74
74
  * foo = any.unpack(Foo.class);
75
75
  * }
76
- * // or ...
77
- * if (any.isSameTypeAs(Foo.getDefaultInstance())) {
78
- * foo = any.unpack(Foo.getDefaultInstance());
79
- * }
80
76
  *
81
77
  * Example 3: Pack and unpack a message in Python.
82
78
  *
@@ -91,13 +87,10 @@ import { MessageType } from "@protobuf-ts/runtime";
91
87
  * Example 4: Pack and unpack a message in Go
92
88
  *
93
89
  * foo := &pb.Foo{...}
94
- * any, err := anypb.New(foo)
95
- * if err != nil {
96
- * ...
97
- * }
90
+ * any, err := ptypes.MarshalAny(foo)
98
91
  * ...
99
92
  * foo := &pb.Foo{}
100
- * if err := any.UnmarshalTo(foo); err != nil {
93
+ * if err := ptypes.UnmarshalAny(any, foo); err != nil {
101
94
  * ...
102
95
  * }
103
96
  *
@@ -107,6 +100,7 @@ import { MessageType } from "@protobuf-ts/runtime";
107
100
  * in the type URL, for example "foo.bar.com/x/y.z" will yield type
108
101
  * name "y.z".
109
102
  *
103
+ *
110
104
  * JSON
111
105
  * ====
112
106
  * The JSON representation of an `Any` value uses the regular
@@ -163,8 +157,7 @@ export interface Any {
163
157
  *
164
158
  * Note: this functionality is not currently available in the official
165
159
  * protobuf release, and it is not used for type URLs beginning with
166
- * type.googleapis.com. As of May 2023, there are no widely used type server
167
- * implementations and no plans to implement one.
160
+ * type.googleapis.com.
168
161
  *
169
162
  * Schemes other than `http`, `https` (or the empty scheme) might be
170
163
  * used with implementation specific semantics.
@@ -49,6 +49,7 @@ import { MessageType } from "@protobuf-ts/runtime";
49
49
  * rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
50
50
  * }
51
51
  *
52
+ * The JSON representation for `Empty` is empty JSON object `{}`.
52
53
  *
53
54
  * @generated from protobuf message google.protobuf.Empty
54
55
  */
@@ -97,15 +97,8 @@ import { MessageType } from "@protobuf-ts/runtime";
97
97
  * Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
98
98
  * .setNanos((int) ((millis % 1000) * 1000000)).build();
99
99
  *
100
- * Example 5: Compute Timestamp from Java `Instant.now()`.
101
100
  *
102
- * Instant now = Instant.now();
103
- *
104
- * Timestamp timestamp =
105
- * Timestamp.newBuilder().setSeconds(now.getEpochSecond())
106
- * .setNanos(now.getNano()).build();
107
- *
108
- * Example 6: Compute Timestamp from current time in Python.
101
+ * Example 5: Compute Timestamp from current time in Python.
109
102
  *
110
103
  * timestamp = Timestamp()
111
104
  * timestamp.GetCurrentTime()
@@ -134,10 +127,11 @@ import { MessageType } from "@protobuf-ts/runtime";
134
127
  * [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
135
128
  * the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
136
129
  * the Joda Time's [`ISODateTimeFormat.dateTime()`](
137
- * http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime()
130
+ * http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D
138
131
  * ) to obtain a formatter capable of generating timestamps in this format.
139
132
  *
140
133
  *
134
+ *
141
135
  * @generated from protobuf message google.protobuf.Timestamp
142
136
  */
143
137
  export interface Timestamp {
@@ -351,9 +351,9 @@ export interface ConnectivityRequest {
351
351
  */
352
352
  host?: string;
353
353
  /**
354
- * @generated from protobuf field: bool selfSigned = 4;
354
+ * @generated from protobuf field: bool allowSelfSignedCertificate = 4;
355
355
  */
356
- selfSigned: boolean;
356
+ allowSelfSignedCertificate: boolean;
357
357
  }
358
358
  /**
359
359
  * @generated from protobuf message dht.ConnectivityResponse
@@ -962,7 +962,7 @@ class ConnectivityRequest$Type extends MessageType<ConnectivityRequest> {
962
962
  { no: 1, name: "port", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
963
963
  { no: 2, name: "tls", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
964
964
  { no: 3, name: "host", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
965
- { no: 4, name: "selfSigned", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
965
+ { no: 4, name: "allowSelfSignedCertificate", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
966
966
  ]);
967
967
  }
968
968
  }
@@ -159,7 +159,7 @@ run().then(() => {
159
159
  console.log(foundData)
160
160
  const layer0Node = currentNode.stack.getLayer0Node() as DhtNode
161
161
  console.log(layer0Node.getNeighbors().length)
162
- console.log(layer0Node.getConnectionCount())
162
+ console.log(layer0Node.getConnectionsView().getConnectionCount())
163
163
  const streamPartDelivery = contentDeliveryManager
164
164
  .getStreamPartDelivery(streamParts[0])! as { layer1Node: Layer1Node, node: ContentDeliveryLayerNode }
165
165
  console.log(streamPartDelivery.layer1Node.getNeighbors())
@@ -29,11 +29,13 @@ describe('ContentDeliveryLayerNode-DhtNode-Latencies', () => {
29
29
 
30
30
  entryPointLayer1Node = new DhtNode({
31
31
  transport: entrypointCm,
32
+ connectionsView: entrypointCm,
32
33
  peerDescriptor: entrypointDescriptor,
33
34
  serviceId: streamPartId
34
35
  })
35
36
  otherLayer1Nodes = range(otherNodeCount).map((i) => new DhtNode({
36
37
  transport: cms[i],
38
+ connectionsView: cms[i],
37
39
  peerDescriptor: peerDescriptors[i],
38
40
  serviceId: streamPartId
39
41
  }))
@@ -44,12 +44,14 @@ describe('ContentDeliveryLayerNode-DhtNode', () => {
44
44
 
45
45
  entryPointLayer1Node = new DhtNode({
46
46
  transport: entrypointCm,
47
+ connectionsView: entrypointCm,
47
48
  peerDescriptor: entrypointDescriptor,
48
49
  serviceId: streamPartId
49
50
  })
50
51
 
51
52
  otherLayer1Nodes = range(otherNodeCount).map((i) => new DhtNode({
52
53
  transport: cms[i],
54
+ connectionsView: cms[i],
53
55
  peerDescriptor: peerDescriptors[i],
54
56
  serviceId: streamPartId
55
57
  }))
@@ -44,11 +44,13 @@ describe('ContentDeliveryManager', () => {
44
44
  await transport2.start()
45
45
  layer0Node1 = new DhtNode({
46
46
  transport: transport1,
47
+ connectionsView: transport1,
47
48
  peerDescriptor: peerDescriptor1,
48
49
  entryPoints: [peerDescriptor1]
49
50
  })
50
51
  layer0Node2 = new DhtNode({
51
52
  transport: transport2,
53
+ connectionsView: transport2,
52
54
  peerDescriptor: peerDescriptor2,
53
55
  entryPoints: [peerDescriptor1]
54
56
  })
@@ -30,7 +30,8 @@ describe('inspect', () => {
30
30
  layer0: {
31
31
  entryPoints: [publisherDescriptor],
32
32
  peerDescriptor,
33
- transport
33
+ transport,
34
+ connectionsView: transport
34
35
  }
35
36
  })
36
37
  await node.start()