@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.
- package/dist/package.json +6 -7
- package/dist/src/NetworkStack.js +1 -1
- package/dist/src/NetworkStack.js.map +1 -1
- package/dist/src/logic/ContentDeliveryLayerNode.js +9 -8
- package/dist/src/logic/ContentDeliveryLayerNode.js.map +1 -1
- package/dist/src/logic/ContentDeliveryManager.d.ts +2 -0
- package/dist/src/logic/ContentDeliveryManager.js +43 -13
- package/dist/src/logic/ContentDeliveryManager.js.map +1 -1
- package/dist/src/logic/EntryPointDiscovery.d.ts +3 -14
- package/dist/src/logic/EntryPointDiscovery.js +9 -83
- package/dist/src/logic/EntryPointDiscovery.js.map +1 -1
- package/dist/src/logic/Layer0Node.d.ts +2 -2
- package/dist/src/logic/Layer1Node.d.ts +7 -3
- package/dist/src/logic/StreamPartNetworkSplitAvoidance.d.ts +18 -0
- package/dist/src/logic/StreamPartNetworkSplitAvoidance.js +73 -0
- package/dist/src/logic/StreamPartNetworkSplitAvoidance.js.map +1 -0
- package/dist/src/logic/StreamPartReconnect.d.ts +11 -0
- package/dist/src/logic/StreamPartReconnect.js +35 -0
- package/dist/src/logic/StreamPartReconnect.js.map +1 -0
- package/dist/src/proto/google/protobuf/any.d.ts +4 -11
- package/dist/src/proto/google/protobuf/any.js.map +1 -1
- package/dist/src/proto/google/protobuf/empty.d.ts +1 -0
- package/dist/src/proto/google/protobuf/empty.js.map +1 -1
- package/dist/src/proto/google/protobuf/timestamp.d.ts +3 -9
- package/dist/src/proto/google/protobuf/timestamp.js.map +1 -1
- package/dist/src/proto/packages/dht/protos/DhtRpc.d.ts +2 -2
- package/dist/src/proto/packages/dht/protos/DhtRpc.js +1 -1
- package/dist/src/proto/packages/dht/protos/DhtRpc.js.map +1 -1
- package/dist/test/benchmark/first-message.js +1 -1
- package/dist/test/benchmark/first-message.js.map +1 -1
- package/dist/test/utils/utils.js +2 -0
- package/dist/test/utils/utils.js.map +1 -1
- package/package.json +6 -7
- package/src/NetworkStack.ts +1 -1
- package/src/logic/ContentDeliveryLayerNode.ts +11 -9
- package/src/logic/ContentDeliveryManager.ts +44 -16
- package/src/logic/EntryPointDiscovery.ts +14 -102
- package/src/logic/Layer0Node.ts +2 -2
- package/src/logic/Layer1Node.ts +7 -3
- package/src/logic/StreamPartNetworkSplitAvoidance.ts +89 -0
- package/src/logic/StreamPartReconnect.ts +37 -0
- package/src/proto/google/protobuf/any.ts +4 -11
- package/src/proto/google/protobuf/empty.ts +1 -0
- package/src/proto/google/protobuf/timestamp.ts +3 -9
- package/src/proto/packages/dht/protos/DhtRpc.ts +3 -3
- package/test/benchmark/first-message.ts +1 -1
- package/test/integration/ContentDeliveryLayerNode-Layer1Node-Latencies.test.ts +2 -0
- package/test/integration/ContentDeliveryLayerNode-Layer1Node.test.ts +2 -0
- package/test/integration/ContentDeliveryManager.test.ts +2 -0
- package/test/integration/Inspect.test.ts +2 -1
- package/test/integration/NetworkNode.test.ts +4 -2
- package/test/integration/NodeInfoRpc.test.ts +2 -0
- package/test/integration/joining-streams-on-offline-peers.test.ts +3 -0
- package/test/integration/stream-without-default-entrypoints.test.ts +2 -0
- package/test/integration/streamEntryPointReplacing.test.ts +2 -0
- package/test/unit/ContentDeliveryLayerNode.test.ts +5 -5
- package/test/unit/ContentDeliveryManager.test.ts +1 -1
- package/test/unit/ContentDeliveryRpcLocal.test.ts +1 -1
- package/test/unit/EntrypointDiscovery.test.ts +9 -40
- package/test/unit/Inspector.test.ts +1 -1
- package/test/unit/NeighborUpdateRpcLocal.test.ts +1 -1
- package/test/unit/NodeList.test.ts +1 -1
- package/test/unit/StreamPartNetworkSplitAvoidance.test.ts +32 -0
- package/test/unit/StreamPartReconnect.test.ts +30 -0
- package/test/unit/TemporaryConnectionRpcLocal.test.ts +1 -1
- package/test/utils/fake/FakeEntryPointDiscovery.ts +29 -0
- package/test/utils/mock/MockConnectionsView.ts +18 -0
- package/test/utils/mock/MockLayer0Node.ts +5 -14
- package/test/utils/mock/MockLayer1Node.ts +2 -2
- package/test/utils/mock/{Transport.ts → MockTransport.ts} +2 -16
- 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 {
|
|
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.
|
|
170
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
58
|
+
async storeAndKeepLocalNodeAsEntryPoint(): Promise<void> {
|
|
122
59
|
if (this.abortController.signal.aborted) {
|
|
123
60
|
return
|
|
124
61
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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.
|
|
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
|
}
|
package/src/logic/Layer0Node.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
+
getConnectionsView(): ConnectionsView
|
|
15
15
|
start(): Promise<void>
|
|
16
16
|
stop(): Promise<void>
|
|
17
17
|
}
|
package/src/logic/Layer1Node.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { DhtAddress, PeerDescriptor, RingContacts } from '@streamr/dht'
|
|
2
2
|
|
|
3
3
|
export interface Layer1NodeEvents {
|
|
4
|
-
|
|
5
|
-
|
|
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 :=
|
|
95
|
-
* if err != nil {
|
|
96
|
-
* ...
|
|
97
|
-
* }
|
|
90
|
+
* any, err := ptypes.MarshalAny(foo)
|
|
98
91
|
* ...
|
|
99
92
|
* foo := &pb.Foo{}
|
|
100
|
-
* if err :=
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
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
|
|
354
|
+
* @generated from protobuf field: bool allowSelfSignedCertificate = 4;
|
|
355
355
|
*/
|
|
356
|
-
|
|
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: "
|
|
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
|
})
|