diff --git a/README.md b/README.md index 0574ea7..354b0e4 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,7 @@ -go-kademlia +go-libdht ======================= -> A Generic Go Kademlia Implementation - -Generic Kademlia implementation in Go that can be used to build DHT Protocols. This repository is **NOT** meant to be the home of the Go IPFS DHT implementation, it is meant to provide generic Kademlia building blocks that could be used in multiple Kademlia implementations and simulations. This Kademlia implementation was built to be reproducible: most of the modules are single threaded, allowing sequential execution. This repository enables deterministic testing, and stable simulations. - -## Documentation - -TBD - -## Design - -[`Design document`](./design/README.md) - -## Lead Maintainer - -[Guillaume Michel](https://github.com/guillaumemichel) +WIP ## Contributing diff --git a/design/README.md b/design/README.md deleted file mode 100644 index 0230029..0000000 --- a/design/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Single Threaded Kademlia Design - -Author: [Guillaume Michel](https://github.com/guillaumemichel) - -## Design overview & rationale - -This Kademlia implementation allows a sequential execution, providing many benefits (such as stable testing, better debuggability, reproducible simulations, etc.). The Kademlia query process is most efficient when multiple requests are sent concurrently. In order to reach both sequential execution and concurrency, this implementation follows a same thread concurrency pattern. As much work as possible will be carried out by the single worker. - -In some cases, it is required that the implementation deals with multiple threads. For instance, callers may call this implementation from multiple threads. Some message endpoint implementations (such as [`libp2p`](../network/endpoint/libp2pendpoint/)), need to have a dedicated thread for sending a request and receiving an answer. - -In order to orchestrate the process, this implementation includes a [`scheduler`](../events/scheduler/), responsible for synchronizing the necessary threads with the single worker. The scheduler offer a thread safe interface to add [`actions`](../events/action/) to the event queue of the single worker to be run as soon as possible, or to schedule an action at a specific time. The actions are simply pointers on functions that are executed when the single worker picks them up. - -![alt text](./excalidraw/scheduler.png) - -### Query process - -For example, when a caller wants to perform a lookup request over libp2p, it will add an action to the scheduler to create a query for specific parameters (e.g `FIND_PEER` and target `peer.ID`). Once available, the single worker will create the associated request, and depending on the concurrency of this request, add `concurrency` `SendRequest` `actions` to the event queue. - -![alt text](./excalidraw/query-setup.png) - -It will then execute the `SendRequest` `actions` one by one. The `SendRequest` action consists in looking up for the closest known peer to the target Kademlia key that hasn't been queried yet, and send it a request over libp2p. As [go-libp2p](https://github.com/libp2p/go-libp2p) needs a dedicated go routine to send a receive message, the single worker creates a new go routine that will handle the network part. The single worker can then pick up the next action in the event queue. - -The libp2p message go routine, will (1) dial the remote peer, (2) send the IPFS Kademlia request, (3) schedule a timeout `action` to the `scheduler`, after the provided timeout value and (4) wait for an answer. If there is an error during this process, the go routine will add an error `action` in the event queue and die. If the go routine receives an answer before the timeout happens, it will add a `HandleResponse` event to the event queue, that will be picked up by the single worker once available, it will remove the scheduled timeout event and then the libp2p message go routine can die in peace. If the libp2p go routine receives a message after the timeout, it can also die in peace. - -If the timeout even is picked up by the single worker, it means that the remote peer has failed to answer on time. The single worker will update the query, and enqueue another `SendRequest` `action`. - -When a `HandleResponse` `action` is picked up by the single worker, it executes the `ResponseHandlerFn` (`func(context.Context, kad.NodeID, message.MinKadResponseMessage) (bool, []kad.NodeID)`) that was provided by the caller when creating the query. This function should return whether the query should terminate, and the list of useful peers to query next. Hence, the caller can keep a state for the query, and decide when it should terminate. If the query shouldn't terminate yet, the single worker updates the query state, and enqueues another `SendRequest` `action`. - -![alt text](./excalidraw/query-run.png) - -This makes the execution as sequential as possible, given that it depends on the network and remote peers. - -### Tests & Simulations - -It is also possible to run a single threaded simulation, for instance to perform tests. The single threaded simulation is strictly sequential, and can be controlled by the programmer. A [`simulation`](../events/simulator/) usually uses a [`fake message endpoint`](../network/endpoint/fakeendpoint/) to disptach messages from one simulated host to another. The simulation can for instance, continuously run one action in each node's scheduler, in a specific order, until they are no more actions to run. This allows accurate testing of the modules. - -### Keys & Addressing - -A [`Kademlia key`](../key/) is simply an slice of bytes of arbitrary length that can be defined by the user. The logic to build Kademlia keys from libp2p `peer.ID` is described in this [module](../key/sha256key256/). - -Node [`addressing`](../network/address/) is as generic as possible to enable multiple DHT implementations to build on top of this library. A `NodeID` should only implement a `Key()` function, mapping to its Kademlia key. - -### Modules - -This repository is composed of modules that can easily be exchanged. For instance, the IPFS DHT implementation will not use all the same modules as a DHT simulation, even though they follow the same logic. Most of the code is reused, and only the addressing, networking part, event loop need to be changed. Another example is that it is very easy for a custom DHT implementation to change it routing table logic, while still taking part in the same DHT network. - -## IPFS DHT Interface - -### Peer Routing - -- `FindPeers([]peer.ID) []peer.AddrInfo`: returns the `[]peer.AddrInfo` associated with the requested `[]peer.ID` -- (`GetClosestPeers(key, n) []peer.AddrInfo`): find the n closest `peer.ID` to the fiven `key` and return their associated `peer.AddrInfo` - -Note that these functions can be sync or async. If they are sync, they block until we get the full result. If async, they return `peer.AddrInfo` as they are discovered. - -Note that the `FindPeers` will be called with a single `peer.ID` in most of the cases. - -### Provider Records - -**Provide** -- `StartProvide([]cid.Cid)`: start providing the given CIDs, and keep republishing them -- `StopProvide([]cid.Cid)`: stop republishing the given CIDs (if they were provided) -- `ListProvide() []cid.Cid`: returns the list of CIDs being republished - -**Lookup** - -- `FindProviders([]cid.Cid) []peer.AddrInfo`: looks for the provider records associated with the requested `[]cid.Cid`, and return the `peer.AddrInfo` of the peers providing it. - -Note that this function can be sync or async. If sync, it blocks until we get the full result. If async, it returns provider records as they are discovered. - -Note that the `FindProviders` will be called with a single `cid.Cid` in most of the cases. - - -### IPNS - -- `PutValues([]key, []value)`: for all key-value pairs, store `value` in the DHT at the location `key`. -- `GetValues([]key) []value`: for each `key`, retrieve the associated `value` and return it - -### Others - -- (`GetPublicKey(PeerID)` ??): this RPC should be dropped. diff --git a/design/excalidraw/event-loop.png b/design/excalidraw/event-loop.png deleted file mode 100644 index 8eb58bd..0000000 Binary files a/design/excalidraw/event-loop.png and /dev/null differ diff --git a/design/excalidraw/kademlia-design.excalidraw b/design/excalidraw/kademlia-design.excalidraw deleted file mode 100644 index 148e79a..0000000 --- a/design/excalidraw/kademlia-design.excalidraw +++ /dev/null @@ -1,17964 +0,0 @@ -{ - "type": "excalidraw", - "version": 2, - "source": "https://excalidraw.com", - "elements": [ - { - "type": "text", - "version": 65, - "versionNonce": 784777617, - "isDeleted": false, - "id": "WUqPSXfwFVxePj242Rn0m", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1256, - "y": 254, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 655.7166748046875, - "height": 87.00000000000003, - "seed": 887705780, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516825, - "link": null, - "locked": false, - "fontSize": 69.60000000000002, - "fontFamily": 1, - "text": "Timeouts & Delays", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Timeouts & Delays", - "lineHeight": 1.25, - "baseline": 61 - }, - { - "type": "text", - "version": 76, - "versionNonce": 388799487, - "isDeleted": false, - "id": "ipwo6ded4RlhllN11MOgs", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1265, - "y": 390, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 309.8500061035156, - "height": 70, - "seed": 144594188, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516825, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Routing Table Refresh\nEvery 10 minutes", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Routing Table Refresh\nEvery 10 minutes", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "text", - "version": 40, - "versionNonce": 880437105, - "isDeleted": false, - "id": "tiVxt5l-bBiuwpq1zLi4F", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1266, - "y": 506, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 253.89999389648438, - "height": 35, - "seed": 717797388, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516825, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Reprovide Interval", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Reprovide Interval", - "lineHeight": 1.25, - "baseline": 25 - }, - { - "type": "text", - "version": 86, - "versionNonce": 878101535, - "isDeleted": false, - "id": "uasTUzzWP8orCFzcm4k6b", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1658, - "y": 378, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 257.01666259765625, - "height": 70, - "seed": 1658148748, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516825, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Request timeouts\n(and sub-timeouts)", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Request timeouts\n(and sub-timeouts)", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "text", - "version": 543, - "versionNonce": 1954435409, - "isDeleted": false, - "id": "4EJ1cEERtKLGQo46O7PAX", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2226, - "y": 312, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 490.8999938964844, - "height": 455, - "seed": 1608726068, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516825, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Each user request should have a\nchannel on which it writes back the\nresults.\nFor sync requests, the go routine\nis blocking until something is written\nin this channel, or context is\ncancelled.\nFor async requests, the result is\ndirectly written to the channel. \n\nIn any case, expensive requests\n(provide, lookup, etc.) should pay\nattention to context.", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Each user request should have a\nchannel on which it writes back the\nresults.\nFor sync requests, the go routine\nis blocking until something is written\nin this channel, or context is\ncancelled.\nFor async requests, the result is\ndirectly written to the channel. \n\nIn any case, expensive requests\n(provide, lookup, etc.) should pay\nattention to context.", - "lineHeight": 1.25, - "baseline": 445 - }, - { - "type": "rectangle", - "version": 181, - "versionNonce": 1447487551, - "isDeleted": false, - "id": "q9VvRLjSHONCHfxtWbkb4", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2062, - "y": 1102, - "strokeColor": "#1e1e1e", - "backgroundColor": "#a5d8ff", - "width": 227, - "height": 295, - "seed": 1590587916, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "type": "text", - "id": "wfSM5O1Wmi1pEszbaFFPs" - }, - { - "id": "477yePyUwfRLqeAcC8MpC", - "type": "arrow" - }, - { - "id": "Fm1doNnsiMN7ruq0_f35v", - "type": "arrow" - }, - { - "id": "IDwvCouKy3OWqtuy0eiX8", - "type": "arrow" - }, - { - "id": "JCP3hRIUAwGN33mi5-sxU", - "type": "arrow" - }, - { - "id": "HhguYZrNsmbCkzE498rRf", - "type": "arrow" - }, - { - "id": "Gl6phd93NyeAPBuyKjfWn", - "type": "arrow" - }, - { - "id": "E3vRedWG-SzQ8jxuvMNmT", - "type": "arrow" - } - ], - "updated": 1687761516825, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 143, - "versionNonce": 1143602993, - "isDeleted": false, - "id": "wfSM5O1Wmi1pEszbaFFPs", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2090.775001525879, - "y": 1232, - "strokeColor": "#1e1e1e", - "backgroundColor": "#a5d8ff", - "width": 169.4499969482422, - "height": 35, - "seed": 588815540, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516825, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Event queue", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "q9VvRLjSHONCHfxtWbkb4", - "originalText": "Event queue", - "lineHeight": 1.25, - "baseline": 25 - }, - { - "type": "diamond", - "version": 152, - "versionNonce": 1792903263, - "isDeleted": false, - "id": "Oduul3CuZ1FYnWab1-DDC", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1846.5, - "y": 1099, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 146, - "height": 110, - "seed": 1297451572, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "bjF1IPS-it9UetOrDENxW" - }, - { - "id": "477yePyUwfRLqeAcC8MpC", - "type": "arrow" - } - ], - "updated": 1687761516825, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 145, - "versionNonce": 1642393873, - "isDeleted": false, - "id": "bjF1IPS-it9UetOrDENxW", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1889.5416660308838, - "y": 1134, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 59.91666793823242, - "height": 40, - "seed": 372784268, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516825, - "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 1, - "text": "app \nrequest", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "Oduul3CuZ1FYnWab1-DDC", - "originalText": "app request", - "lineHeight": 1.25, - "baseline": 35 - }, - { - "type": "diamond", - "version": 255, - "versionNonce": 1340810367, - "isDeleted": false, - "id": "xmP-R27KRedrS73wM-oNB", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1842.5, - "y": 1300.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 154, - "height": 100, - "seed": 926487988, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "J3jItXEtqVqr9WfBbNJo2" - }, - { - "id": "IDwvCouKy3OWqtuy0eiX8", - "type": "arrow" - } - ], - "updated": 1687761516825, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 292, - "versionNonce": 1395042033, - "isDeleted": false, - "id": "J3jItXEtqVqr9WfBbNJo2", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1886.5166664123535, - "y": 1330.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 65.96666717529297, - "height": 40, - "seed": 544489268, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 1, - "text": "message\nresponse", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "xmP-R27KRedrS73wM-oNB", - "originalText": "message response", - "lineHeight": 1.25, - "baseline": 35 - }, - { - "type": "rectangle", - "version": 215, - "versionNonce": 1429224607, - "isDeleted": false, - "id": "4ILlyyPtaYQg5jdeDCtq6", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2829, - "y": 1004, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 379.99999999999983, - "height": 346, - "seed": 595499828, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "qwaB-FTBsRdMcXXlJS5Gr", - "type": "arrow" - }, - { - "id": "SNc1oEUDb1Y4R-qzWj-8n", - "type": "arrow" - }, - { - "id": "JCP3hRIUAwGN33mi5-sxU", - "type": "arrow" - }, - { - "id": "KbUdN9jSlXripDlWC4t9n", - "type": "arrow" - } - ], - "updated": 1687761516826, - "link": null, - "locked": false - }, - { - "type": "diamond", - "version": 302, - "versionNonce": 41328849, - "isDeleted": false, - "id": "HjS18UgrQpsfK-Uw7Y1Cy", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1842.5, - "y": 1211.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 154, - "height": 90, - "seed": 507818164, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "ppYLujySDf45m00WhkPWA" - }, - { - "id": "Fm1doNnsiMN7ruq0_f35v", - "type": "arrow" - } - ], - "updated": 1687761516826, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 344, - "versionNonce": 2120044735, - "isDeleted": false, - "id": "ppYLujySDf45m00WhkPWA", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1909.333333015442, - "y": 1246.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 20.33333396911621, - "height": 20, - "seed": 2035579444, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 1, - "text": "IO", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "HjS18UgrQpsfK-Uw7Y1Cy", - "originalText": "IO", - "lineHeight": 1.25, - "baseline": 15 - }, - { - "type": "diamond", - "version": 301, - "versionNonce": 493641393, - "isDeleted": false, - "id": "pax9ePN47wvxvWWqtJlLf", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2866.5, - "y": 1026.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 154, - "height": 108, - "seed": 1329401484, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "Vjdq7ztYIzHWACsPmpW5E" - } - ], - "updated": 1687761516826, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 348, - "versionNonce": 85123295, - "isDeleted": false, - "id": "Vjdq7ztYIzHWACsPmpW5E", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2911.658332824707, - "y": 1060.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 63.68333435058594, - "height": 40, - "seed": 875781388, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 1, - "text": "message\ntimeout", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "pax9ePN47wvxvWWqtJlLf", - "originalText": "message timeout", - "lineHeight": 1.25, - "baseline": 35 - }, - { - "type": "diamond", - "version": 344, - "versionNonce": 850354321, - "isDeleted": false, - "id": "8F7ElWnRom2BKPEGOAfvt", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2866.5, - "y": 1135.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 154, - "height": 100, - "seed": 1134791476, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "4OMLk59aiglTY189m8r4Q" - } - ], - "updated": 1687761516826, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 412, - "versionNonce": 450981119, - "isDeleted": false, - "id": "4OMLk59aiglTY189m8r4Q", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2915.6833324432373, - "y": 1165.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 55.63333511352539, - "height": 40, - "seed": 1928594612, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 1, - "text": "RT \nrefresh", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "8F7ElWnRom2BKPEGOAfvt", - "originalText": "RT refresh", - "lineHeight": 1.25, - "baseline": 35 - }, - { - "type": "diamond", - "version": 336, - "versionNonce": 267855473, - "isDeleted": false, - "id": "n9FBPt9pD8QF_Z1qpkhXC", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2865, - "y": 1232.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 157, - "height": 96, - "seed": 1747556276, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "Suj4K9gun8F9sl2Euodsx" - } - ], - "updated": 1687761516826, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 446, - "versionNonce": 1799976223, - "isDeleted": false, - "id": "Suj4K9gun8F9sl2Euodsx", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2909.2583351135254, - "y": 1270.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 68.98332977294922, - "height": 20, - "seed": 604274996, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 1, - "text": "reprovide", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "n9FBPt9pD8QF_Z1qpkhXC", - "originalText": "reprovide", - "lineHeight": 1.25, - "baseline": 15 - }, - { - "type": "diamond", - "version": 239, - "versionNonce": 1611346001, - "isDeleted": false, - "id": "MDKv1C1WNItfMV3j1ytGh", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2928.5, - "y": 821, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 158, - "height": 108, - "seed": 1765034548, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "m5H4OOOAROZSbrm3guZZs" - }, - { - "id": "qwaB-FTBsRdMcXXlJS5Gr", - "type": "arrow" - } - ], - "updated": 1687761516826, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 300, - "versionNonce": 1572567359, - "isDeleted": false, - "id": "m5H4OOOAROZSbrm3guZZs", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2973.4916648864746, - "y": 855, - "strokeColor": "#1e1e1e", - "backgroundColor": "#b2f2bb", - "width": 68.01667022705078, - "height": 40, - "seed": 500619020, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 1, - "text": "Initialise\nKad", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "MDKv1C1WNItfMV3j1ytGh", - "originalText": "Initialise Kad", - "lineHeight": 1.25, - "baseline": 35 - }, - { - "type": "arrow", - "version": 527, - "versionNonce": 146996785, - "isDeleted": false, - "id": "qwaB-FTBsRdMcXXlJS5Gr", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3007.9683903797372, - "y": 929.1903671578309, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 3.9386580899213186, - "height": 70.26134973759235, - "seed": 616118668, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "startBinding": { - "elementId": "MDKv1C1WNItfMV3j1ytGh", - "focus": 0.03252370395290413, - "gap": 1 - }, - "endBinding": { - "elementId": "4ILlyyPtaYQg5jdeDCtq6", - "focus": 0.01432121723546135, - "gap": 4.54828310457674 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 3.9386580899213186, - 70.26134973759235 - ] - ] - }, - { - "type": "arrow", - "version": 162, - "versionNonce": 395835743, - "isDeleted": false, - "id": "477yePyUwfRLqeAcC8MpC", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1994, - "y": 1155, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 63, - "height": 0, - "seed": 1326797876, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "startBinding": { - "elementId": "Oduul3CuZ1FYnWab1-DDC", - "focus": 0.01818181818181818, - "gap": 1.7013088372297744 - }, - "endBinding": { - "elementId": "q9VvRLjSHONCHfxtWbkb4", - "focus": 0.6406779661016949, - "gap": 5 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 63, - 0 - ] - ] - }, - { - "type": "arrow", - "version": 163, - "versionNonce": 1225134097, - "isDeleted": false, - "id": "Fm1doNnsiMN7ruq0_f35v", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1996, - "y": 1257, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 59, - "height": 1, - "seed": 370597940, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "startBinding": { - "elementId": "HjS18UgrQpsfK-Uw7Y1Cy", - "focus": 0.03992467043314501, - "gap": 1 - }, - "endBinding": { - "elementId": "q9VvRLjSHONCHfxtWbkb4", - "focus": -0.029832123411978217, - "gap": 7 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 59, - -1 - ] - ] - }, - { - "type": "arrow", - "version": 166, - "versionNonce": 1043297663, - "isDeleted": false, - "id": "IDwvCouKy3OWqtuy0eiX8", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1998, - "y": 1351, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 56, - "height": 1, - "seed": 1218879756, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "startBinding": { - "elementId": "xmP-R27KRedrS73wM-oNB", - "focus": 0.038035714285714284, - "gap": 1.2362543552767775 - }, - "endBinding": { - "elementId": "q9VvRLjSHONCHfxtWbkb4", - "focus": -0.657610318265958, - "gap": 8 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 56, - -1 - ] - ] - }, - { - "type": "freedraw", - "version": 112, - "versionNonce": 382461425, - "isDeleted": false, - "id": "_tLF_WwAW9TWJtS0iwsq-", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1828, - "y": 1088, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 56, - "height": 296, - "seed": 922887948, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -1, - 0 - ], - [ - -5, - -2 - ], - [ - -7, - -2 - ], - [ - -8, - 0 - ], - [ - -9, - 0 - ], - [ - -11, - 1 - ], - [ - -11, - 3 - ], - [ - -11, - 4 - ], - [ - -12, - 5 - ], - [ - -14, - 10 - ], - [ - -16, - 25 - ], - [ - -16, - 26 - ], - [ - -15, - 45 - ], - [ - -15, - 49 - ], - [ - -12, - 66 - ], - [ - -12, - 77 - ], - [ - -12, - 81 - ], - [ - -14, - 87 - ], - [ - -14, - 92 - ], - [ - -15, - 108 - ], - [ - -15, - 113 - ], - [ - -16, - 124 - ], - [ - -21, - 141 - ], - [ - -25, - 152 - ], - [ - -28, - 159 - ], - [ - -28, - 161 - ], - [ - -39, - 148 - ], - [ - -39, - 147 - ], - [ - -37, - 147 - ], - [ - -36, - 148 - ], - [ - -35, - 150 - ], - [ - -32, - 152 - ], - [ - -25, - 164 - ], - [ - -19, - 171 - ], - [ - -11, - 183 - ], - [ - -5, - 194 - ], - [ - -4, - 210 - ], - [ - -5, - 218 - ], - [ - -8, - 234 - ], - [ - -15, - 270 - ], - [ - -16, - 277 - ], - [ - -19, - 283 - ], - [ - -19, - 285 - ], - [ - -18, - 285 - ], - [ - -11, - 290 - ], - [ - -5, - 292 - ], - [ - -1, - 294 - ], - [ - 0, - 294 - ], - [ - 12, - 294 - ], - [ - 13, - 294 - ], - [ - 17, - 291 - ], - [ - 16, - 291 - ], - [ - 13, - 288 - ], - [ - 13, - 288 - ] - ], - "lastCommittedPoint": null, - "simulatePressure": true, - "pressures": [] - }, - { - "type": "text", - "version": 120, - "versionNonce": 2026007967, - "isDeleted": false, - "id": "_Qmd3ycavmnptGitfh1JR", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1649, - "y": 1200, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 126.73332977294922, - "height": 70, - "seed": 1312021772, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Different\nthreads", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Different\nthreads", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "rectangle", - "version": 265, - "versionNonce": 1025245137, - "isDeleted": false, - "id": "0ud0AmYC6gddWR-C58WID", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2405, - "y": 1345, - "strokeColor": "#1e1e1e", - "backgroundColor": "#b2f2bb", - "width": 177, - "height": 83, - "seed": 1318048308, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "type": "text", - "id": "EZzAsU0vEgDFhtoyFH6F2" - }, - { - "id": "ETIHP2TirXnnZpUZXpjim", - "type": "arrow" - }, - { - "id": "HhguYZrNsmbCkzE498rRf", - "type": "arrow" - }, - { - "id": "Gl6phd93NyeAPBuyKjfWn", - "type": "arrow" - } - ], - "updated": 1687761516826, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 253, - "versionNonce": 847339967, - "isDeleted": false, - "id": "EZzAsU0vEgDFhtoyFH6F2", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2459.091667175293, - "y": 1369, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 68.81666564941406, - "height": 35, - "seed": 884170292, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Sleep", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "0ud0AmYC6gddWR-C58WID", - "originalText": "Sleep", - "lineHeight": 1.25, - "baseline": 25 - }, - { - "type": "rectangle", - "version": 76, - "versionNonce": 973987249, - "isDeleted": false, - "id": "fWJA7R6CplLSfLQ-Oit0y", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2292, - "y": 1681, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 420, - "height": 181, - "seed": 652025740, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "type": "text", - "id": "_l3GKpMgfEqIJiZm5vvUu" - }, - { - "id": "ETIHP2TirXnnZpUZXpjim", - "type": "arrow" - }, - { - "id": "SNc1oEUDb1Y4R-qzWj-8n", - "type": "arrow" - }, - { - "id": "E3vRedWG-SzQ8jxuvMNmT", - "type": "arrow" - }, - { - "id": "KbUdN9jSlXripDlWC4t9n", - "type": "arrow" - } - ], - "updated": 1687761516826, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 73, - "versionNonce": 478990815, - "isDeleted": false, - "id": "_l3GKpMgfEqIJiZm5vvUu", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2401, - "y": 1754, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 202, - "height": 35, - "seed": 150098356, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516826, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Perform action", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "fWJA7R6CplLSfLQ-Oit0y", - "originalText": "Perform action", - "lineHeight": 1.25, - "baseline": 25 - }, - { - "type": "arrow", - "version": 467, - "versionNonce": 1556676497, - "isDeleted": false, - "id": "ETIHP2TirXnnZpUZXpjim", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2491.108466855666, - "y": 1436.5924273621597, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 4.267692308660571, - "height": 235.81514527568083, - "seed": 763181964, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "startBinding": { - "elementId": "0ud0AmYC6gddWR-C58WID", - "focus": 0.03695291757985404, - "gap": 8.592427362159697 - }, - "endBinding": { - "elementId": "fWJA7R6CplLSfLQ-Oit0y", - "focus": -0.022824387701928375, - "gap": 8.59242736215947 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 4.267692308660571, - 235.81514527568083 - ] - ] - }, - { - "type": "arrow", - "version": 290, - "versionNonce": 1878343167, - "isDeleted": false, - "id": "SNc1oEUDb1Y4R-qzWj-8n", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2723.677419354839, - "y": 1766.3896555292663, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 318.0880177662, - "height": 401.841290310757, - "seed": 1846030988, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "startBinding": { - "elementId": "fWJA7R6CplLSfLQ-Oit0y", - "focus": 0.5026824520029476, - "gap": 11.677419354839003 - }, - "endBinding": { - "elementId": "4ILlyyPtaYQg5jdeDCtq6", - "focus": -0.2722704639689509, - "gap": 14.548365218509389 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 258.4400306588759, - -112.62945554486994 - ], - [ - 318.0880177662, - -401.841290310757 - ] - ] - }, - { - "type": "text", - "version": 134, - "versionNonce": 595912049, - "isDeleted": false, - "id": "jsHfSaxlmsLnhVAfXQ7bP", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3048.6166648864746, - "y": 1164, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 126.76667022705078, - "height": 35, - "seed": 141795596, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Scheduler", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Scheduler", - "lineHeight": 1.25, - "baseline": 25 - }, - { - "type": "text", - "version": 259, - "versionNonce": 1874535967, - "isDeleted": false, - "id": "BVKQAG_1kTXQcLldm1Onm", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2997.949996948242, - "y": 1649, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 200.10000610351562, - "height": 50, - "seed": 1492788748, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "check if a scheduled\ntask is overdue", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "check if a scheduled\ntask is overdue", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 113, - "versionNonce": 1502154577, - "isDeleted": false, - "id": "JCP3hRIUAwGN33mi5-sxU", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2827, - "y": 1107, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 529.9979437459145, - "height": 42.0801012193048, - "seed": 1097080716, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "startBinding": { - "elementId": "4ILlyyPtaYQg5jdeDCtq6", - "focus": 0.421491654688071, - "gap": 2 - }, - "endBinding": { - "elementId": "q9VvRLjSHONCHfxtWbkb4", - "focus": -0.5167255906158497, - "gap": 8.002056254085346 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -268.9839188184153, - 7.013350203217467 - ], - [ - -529.9979437459145, - 42.0801012193048 - ] - ] - }, - { - "type": "text", - "version": 112, - "versionNonce": 718211647, - "isDeleted": false, - "id": "yt7jBNQdUM793lubfdugw", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2435.449996948242, - "y": 1055, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 179.10000610351562, - "height": 50, - "seed": 985680436, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "no overdue task,\ncheck event queue", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "no overdue task,\ncheck event queue", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 230, - "versionNonce": 1965882673, - "isDeleted": false, - "id": "HhguYZrNsmbCkzE498rRf", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2295, - "y": 1161, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 205.1878135945458, - "height": 172, - "seed": 298277132, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "startBinding": { - "elementId": "q9VvRLjSHONCHfxtWbkb4", - "focus": -0.705858846674044, - "gap": 6 - }, - "endBinding": { - "elementId": "0ud0AmYC6gddWR-C58WID", - "focus": 0.24441324186352245, - "gap": 12 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 169, - 67 - ], - [ - 205.1878135945458, - 172 - ] - ] - }, - { - "type": "text", - "version": 264, - "versionNonce": 2115070559, - "isDeleted": false, - "id": "QGo0KR8pNLgrUJVhfinx5", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2315.491668701172, - "y": 1230, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 155.01666259765625, - "height": 75, - "seed": 814248844, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "if queue empty\nsleep until next\nscheduled event", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "if queue empty\nsleep until next\nscheduled event", - "lineHeight": 1.25, - "baseline": 68 - }, - { - "type": "arrow", - "version": 125, - "versionNonce": 903360273, - "isDeleted": false, - "id": "Gl6phd93NyeAPBuyKjfWn", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2298, - "y": 1368.374939996867, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 102, - "height": 2.152745562659902, - "seed": 1296465972, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "startBinding": { - "elementId": "q9VvRLjSHONCHfxtWbkb4", - "focus": 0.7758042642400808, - "gap": 9 - }, - "endBinding": { - "elementId": "0ud0AmYC6gddWR-C58WID", - "focus": 0.3227961022278052, - "gap": 5 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 102, - 2.152745562659902 - ] - ] - }, - { - "type": "text", - "version": 86, - "versionNonce": 1448265343, - "isDeleted": false, - "id": "0TDr1th46V7Nais-EHuKk", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2293.2916679382324, - "y": 1396, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 95.41666412353516, - "height": 50, - "seed": 1724615820, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "wake on\nnew event", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "wake on\nnew event", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 148, - "versionNonce": 33694961, - "isDeleted": false, - "id": "E3vRedWG-SzQ8jxuvMNmT", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2302, - "y": 1160.0042197289642, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 338.99999999999983, - "height": 507.9957802710359, - "seed": 970270132, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "startBinding": { - "elementId": "q9VvRLjSHONCHfxtWbkb4", - "focus": -0.6971686055174204, - "gap": 13 - }, - "endBinding": { - "elementId": "fWJA7R6CplLSfLQ-Oit0y", - "focus": 0.680241339307998, - "gap": 12.999999999999773 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 300.1147058823528, - 84.49211129764663 - ], - [ - 338.99999999999983, - 507.9957802710359 - ] - ] - }, - { - "type": "text", - "version": 139, - "versionNonce": 844340895, - "isDeleted": false, - "id": "cIglnxJrTIp_Skk-8scpS", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2540.466667175293, - "y": 1162, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 161.06666564941406, - "height": 50, - "seed": 1907746228, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "take next event\n in queue", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "take next event\n in queue", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 198, - "versionNonce": 87187153, - "isDeleted": false, - "id": "KbUdN9jSlXripDlWC4t9n", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2825, - "y": 1114, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 142, - "height": 553, - "seed": 1640289204, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "startBinding": { - "elementId": "4ILlyyPtaYQg5jdeDCtq6", - "focus": 0.8024918471444102, - "gap": 4 - }, - "endBinding": { - "elementId": "fWJA7R6CplLSfLQ-Oit0y", - "focus": 0.7235254709093081, - "gap": 14 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -69, - 126 - ], - [ - -142, - 553 - ] - ] - }, - { - "type": "text", - "version": 78, - "versionNonce": 1690596031, - "isDeleted": false, - "id": "Z6uNTykbl3Qh5M8kI1bEI", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2732, - "y": 1402, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffc9c9", - "width": 130, - "height": 50, - "seed": 1117866804, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "handle\noverdue task", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "handle\noverdue task", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 110, - "versionNonce": 234904753, - "isDeleted": false, - "id": "aTaBt5EqDKY8ISdyHXrKP", - "fillStyle": "cross-hatch", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2564.208335876465, - "y": 849, - "strokeColor": "#e03131", - "backgroundColor": "#ffc9c9", - "width": 229.5833282470703, - "height": 45, - "seed": 1395075636, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Single thread", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Single thread", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "line", - "version": 693, - "versionNonce": 373670623, - "isDeleted": false, - "id": "3YTymS9pz45asLCb3j5-K", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3021, - "y": 801, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "width": 1140, - "height": 1098, - "seed": 1860182412, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516827, - "link": null, - "locked": false, - "startBinding": null, - "endBinding": null, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": null, - "points": [ - [ - 0, - 0 - ], - [ - -630, - 74 - ], - [ - -923, - 349 - ], - [ - -907, - 471 - ], - [ - -613, - 543 - ], - [ - -614, - 645 - ], - [ - -710, - 773 - ], - [ - -706, - 1084 - ], - [ - 71, - 1073 - ], - [ - 217, - 774 - ], - [ - 195, - 174 - ], - [ - -24, - -14 - ] - ] - }, - { - "type": "rectangle", - "version": 62, - "versionNonce": 739483281, - "isDeleted": false, - "id": "g9aaR5wHD6kGZS7RAaW7p", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4456, - "y": 378, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 389, - "height": 325, - "seed": 1925715124, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "YsXMCtmqO97xpw5vYZRVr", - "type": "arrow" - }, - { - "id": "TgjnmKtKGEXwmvknoVaQj", - "type": "arrow" - } - ], - "updated": 1687761516827, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 80, - "versionNonce": 124260095, - "isDeleted": false, - "id": "kStsCu6n6dKS7Z-BfNIgJ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4545.358329772949, - "y": 385, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 211.28334045410156, - "height": 45, - "seed": 1497950092, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Query event", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Query event", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "text", - "version": 48, - "versionNonce": 237216881, - "isDeleted": false, - "id": "lSbKVvxeohZK4w6wjClSp", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4495.716667175293, - "y": 474, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 32.56666564941406, - "height": 25, - "seed": 1934071988, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "ctx", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "ctx", - "lineHeight": 1.25, - "baseline": 18 - }, - { - "type": "text", - "version": 77, - "versionNonce": 313251615, - "isDeleted": false, - "id": "_eysuIGEP9kYZ3QuofLjd", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4488.1083335876465, - "y": 515, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 121.78333282470703, - "height": 25, - "seed": 943573388, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "results chan", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "results chan", - "lineHeight": 1.25, - "baseline": 18 - }, - { - "type": "text", - "version": 116, - "versionNonce": 536342097, - "isDeleted": false, - "id": "eTSopzTLY-dGCbE2FvYBD", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4482.841667175293, - "y": 563, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 188.31666564941406, - "height": 50, - "seed": 1622725812, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "type+target\n(findnode/findprovs)", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "type+target\n(findnode/findprovs)", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 53, - "versionNonce": 1853987647, - "isDeleted": false, - "id": "Q_9LcII5keBhVaoP30b_q", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4489, - "y": 636, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 129.71665954589844, - "height": 25, - "seed": 2131600180, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "queried peers", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "queried peers", - "lineHeight": 1.25, - "baseline": 18 - }, - { - "type": "rectangle", - "version": 91, - "versionNonce": 1751645233, - "isDeleted": false, - "id": "FQ6K-ytKn0egz9-c5HDzV", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4917, - "y": 776, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 240, - "height": 209, - "seed": 1706584332, - "groupIds": [ - "facVbxq37h0LktoqX0Vmn" - ], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "TgjnmKtKGEXwmvknoVaQj", - "type": "arrow" - }, - { - "id": "yTMzJd05EGYH7iFb_ze8O", - "type": "arrow" - } - ], - "updated": 1687761516828, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 143, - "versionNonce": 793369439, - "isDeleted": false, - "id": "PoPOckfGswB9FBWLPNrOx", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4947, - "y": 794, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 172.93333435058594, - "height": 50, - "seed": 1571226252, - "groupIds": [ - "facVbxq37h0LktoqX0Vmn" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "new go routine to\nwait on response", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "new go routine to\nwait on response", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 111, - "versionNonce": 660295185, - "isDeleted": false, - "id": "od1K0KlntJVFAJga7G5AB", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4946, - "y": 918, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 149.35000610351562, - "height": 50, - "seed": 519292340, - "groupIds": [ - "facVbxq37h0LktoqX0Vmn" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "keep pointer to\nquery", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "keep pointer to\nquery", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 165, - "versionNonce": 156571519, - "isDeleted": false, - "id": "QMhNqSodaqC5rAdzuuPQg", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4944, - "y": 858, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 198.9499969482422, - "height": 50, - "seed": 2140279476, - "groupIds": [ - "facVbxq37h0LktoqX0Vmn" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "create new stream\nwith cancelable ctx ", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "create new stream\nwith cancelable ctx ", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "rectangle", - "version": 95, - "versionNonce": 868836337, - "isDeleted": false, - "id": "GuEYPLGsDn3J9P078cKy4", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4531, - "y": 781, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 224, - "height": 197, - "seed": 283204492, - "groupIds": [ - "HpWLxG4eel3VwHvTLuWGu" - ], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "yTMzJd05EGYH7iFb_ze8O", - "type": "arrow" - }, - { - "id": "x16Ry68xoOlSQwewBLQTm", - "type": "arrow" - } - ], - "updated": 1687761516828, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 93, - "versionNonce": 1390661535, - "isDeleted": false, - "id": "A13zZuPpMIOPtPfojERT0", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4557, - "y": 793, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 164.13333129882812, - "height": 50, - "seed": 1057696524, - "groupIds": [ - "HpWLxG4eel3VwHvTLuWGu" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "on response, add\nevent to queue", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "on response, add\nevent to queue", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 137, - "versionNonce": 431603153, - "isDeleted": false, - "id": "9N4tKVe3gnflY72eeHgOY", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4554, - "y": 879, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 181.86666870117188, - "height": 75, - "seed": 681312180, - "groupIds": [ - "HpWLxG4eel3VwHvTLuWGu" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "event must include\nquery pointer + \nthe response", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "event must include\nquery pointer + \nthe response", - "lineHeight": 1.25, - "baseline": 68 - }, - { - "type": "arrow", - "version": 87, - "versionNonce": 1176929215, - "isDeleted": false, - "id": "TgjnmKtKGEXwmvknoVaQj", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4849, - "y": 523, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 158, - "height": 240, - "seed": 1241470732, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "startBinding": { - "elementId": "g9aaR5wHD6kGZS7RAaW7p", - "focus": -0.39635633338115045, - "gap": 4 - }, - "endBinding": { - "elementId": "FQ6K-ytKn0egz9-c5HDzV", - "focus": -0.1029649171161948, - "gap": 13 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 132, - 51 - ], - [ - 158, - 240 - ] - ] - }, - { - "type": "arrow", - "version": 31, - "versionNonce": 2017713073, - "isDeleted": false, - "id": "yTMzJd05EGYH7iFb_ze8O", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4909, - "y": 886, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 148, - "height": 1, - "seed": 949711156, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "startBinding": { - "elementId": "FQ6K-ytKn0egz9-c5HDzV", - "focus": -0.044013858591043245, - "gap": 8 - }, - "endBinding": { - "elementId": "GuEYPLGsDn3J9P078cKy4", - "focus": 0.08359428182437031, - "gap": 6 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -148, - 1 - ] - ] - }, - { - "type": "rectangle", - "version": 114, - "versionNonce": 54375391, - "isDeleted": false, - "id": "W_Z0rPIVWqCrP39-hy4mg", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3877, - "y": 498, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 250, - "height": 354, - "seed": 2084074124, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "x16Ry68xoOlSQwewBLQTm", - "type": "arrow" - }, - { - "id": "IKGx3K3mxf_QNCg5IA0yx", - "type": "arrow" - }, - { - "id": "qvKn8boKDYANLHE5fhjMf", - "type": "arrow" - } - ], - "updated": 1687761516828, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 89, - "versionNonce": 529481105, - "isDeleted": false, - "id": "fLc6-VRFV1V_Z4ZKjXu4b", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3895, - "y": 512, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 217.89999389648438, - "height": 45, - "seed": 1416487988, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Event queue", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Event queue", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "arrow", - "version": 159, - "versionNonce": 1379524607, - "isDeleted": false, - "id": "x16Ry68xoOlSQwewBLQTm", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4522.999999999999, - "y": 862.7541353867622, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 390.1523837631997, - "height": 55.9571727212924, - "seed": 15180340, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "startBinding": { - "elementId": "GuEYPLGsDn3J9P078cKy4", - "focus": 0.30453971636227506, - "gap": 8.00000000000091 - }, - "endBinding": { - "elementId": "W_Z0rPIVWqCrP39-hy4mg", - "focus": 0.535101323655333, - "gap": 5.847616236799695 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -241.2368630093506, - 37.21787954096931 - ], - [ - -390.1523837631997, - -18.739293180323095 - ] - ] - }, - { - "type": "rectangle", - "version": 37, - "versionNonce": 1859347313, - "isDeleted": false, - "id": "gDgEr4ejIwqUnoTzWLJLF", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4191, - "y": 503, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 195, - "height": 183, - "seed": 296061196, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "IKGx3K3mxf_QNCg5IA0yx", - "type": "arrow" - }, - { - "id": "YsXMCtmqO97xpw5vYZRVr", - "type": "arrow" - } - ], - "updated": 1687761516828, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 70, - "versionNonce": 622561311, - "isDeleted": false, - "id": "ly3EndTPeqdTA01qYbOt5", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4236, - "y": 509, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 105.30000305175781, - "height": 50, - "seed": 1120957876, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "when event\npicked up", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "when event\npicked up", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 79, - "versionNonce": 1918356817, - "isDeleted": false, - "id": "PN9MW97gUvfdBGpinwX9p", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4210, - "y": 585, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 158.53334045410156, - "height": 50, - "seed": 991609524, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516828, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "update query\nevent and go on", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "update query\nevent and go on", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 28, - "versionNonce": 1507011647, - "isDeleted": false, - "id": "IKGx3K3mxf_QNCg5IA0yx", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4137, - "y": 633, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 47, - "height": 34, - "seed": 2040905228, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516829, - "link": null, - "locked": false, - "startBinding": { - "elementId": "W_Z0rPIVWqCrP39-hy4mg", - "focus": 0.2081311162383642, - "gap": 10 - }, - "endBinding": { - "elementId": "gDgEr4ejIwqUnoTzWLJLF", - "focus": 0.4387761801588865, - "gap": 7 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 47, - -34 - ] - ] - }, - { - "type": "arrow", - "version": 28, - "versionNonce": 1098119985, - "isDeleted": false, - "id": "YsXMCtmqO97xpw5vYZRVr", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4390, - "y": 585, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 55, - "height": 12, - "seed": 949608204, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "startBinding": { - "elementId": "gDgEr4ejIwqUnoTzWLJLF", - "focus": 0.1121322047561467, - "gap": 4 - }, - "endBinding": { - "elementId": "g9aaR5wHD6kGZS7RAaW7p", - "focus": 0.06019606973339839, - "gap": 11 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 55, - -12 - ] - ] - }, - { - "type": "text", - "version": 122, - "versionNonce": 1010912351, - "isDeleted": false, - "id": "vetQ1EbAw1GdBlFdUIQkB", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4173, - "y": 1079, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 741.2666625976562, - "height": 38.999999999999986, - "seed": 149690036, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 31.19999999999999, - "fontFamily": 1, - "text": "repeat until query is finished or context is over", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "repeat until query is finished or context is over", - "lineHeight": 1.25, - "baseline": 28 - }, - { - "type": "ellipse", - "version": 137, - "versionNonce": 479161617, - "isDeleted": false, - "id": "ah1HyWxIndiqZzZqbxwa_", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4070, - "y": 663, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 1257, - "height": 391.99999999999983, - "seed": 1542600500, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 58, - "versionNonce": 1211525247, - "isDeleted": false, - "id": "dqW7xLw-gouRuPGHLnyHU", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4987, - "y": 536, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 93.6500015258789, - "height": 50, - "seed": 875352076, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "query new\npeer", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "query new\npeer", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 97, - "versionNonce": 1260495601, - "isDeleted": false, - "id": "-guiNzktMsqAWprr6lbAv", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4215.725006103516, - "y": 939, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 266.54998779296875, - "height": 35, - "seed": 1653406260, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Message go routine", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Message go routine", - "lineHeight": 1.25, - "baseline": 25 - }, - { - "type": "ellipse", - "version": 123, - "versionNonce": 728957087, - "isDeleted": false, - "id": "52OJYS4rpB2NSx4xGMLp5", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 6.080700732737872, - "x": 3627.81708706936, - "y": 348.3392471485706, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "width": 1432.8956213330428, - "height": 474.6763159040205, - "seed": 1436578484, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 50, - "versionNonce": 967974097, - "isDeleted": false, - "id": "mEB2x1-qBr9aS3xqgdAJK", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 6.080700732737872, - "x": 4129.650001525879, - "y": 390, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "width": 210.6999969482422, - "height": 35, - "seed": 2027919116, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Main go routine", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Main go routine", - "lineHeight": 1.25, - "baseline": 25 - }, - { - "type": "rectangle", - "version": 117, - "versionNonce": 128423103, - "isDeleted": false, - "id": "DrI2sQu4cd-iElBWdhcKM", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3735, - "y": 230, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 160.99999999999994, - "height": 117, - "seed": 192562700, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "NRKYPXJcdJvrdiAUHOpwO", - "type": "arrow" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 149, - "versionNonce": 1473796785, - "isDeleted": false, - "id": "flCfhPTBLSgo3slrI5KnZ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3754.31353329074, - "y": 251, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 131.63333129882812, - "height": 75.00000000000001, - "seed": 1648969740, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 30.000000000000004, - "fontFamily": 1, - "text": "New user\nrequest", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "New user\nrequest", - "lineHeight": 1.25, - "baseline": 64 - }, - { - "type": "rectangle", - "version": 59, - "versionNonce": 1953801439, - "isDeleted": false, - "id": "usP9B_26Zn4ILtIt8BOtJ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3651, - "y": 400, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 155, - "height": 162.00000000000003, - "seed": 1412185740, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "NRKYPXJcdJvrdiAUHOpwO", - "type": "arrow" - }, - { - "id": "qvKn8boKDYANLHE5fhjMf", - "type": "arrow" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 84, - "versionNonce": 909892753, - "isDeleted": false, - "id": "jMl140NbBKworLnjFO03c", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3658.241668701172, - "y": 425, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 143.51666259765625, - "height": 50, - "seed": 32311604, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "add request +\nchan to queue", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "add request +\nchan to queue", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 61, - "versionNonce": 1097511167, - "isDeleted": false, - "id": "qUV8Nr9o5GLsdGy3Hp6lI", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3661.408332824707, - "y": 497, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 131.18333435058594, - "height": 50, - "seed": 1577089804, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "wait on chan\nor ctx cancel", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "wait on chan\nor ctx cancel", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 74, - "versionNonce": 1687429745, - "isDeleted": false, - "id": "NRKYPXJcdJvrdiAUHOpwO", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3801.5565578580754, - "y": 350, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 30.11325162617777, - "height": 43, - "seed": 24149772, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "startBinding": { - "elementId": "DrI2sQu4cd-iElBWdhcKM", - "focus": -0.2397792337200884, - "gap": 3 - }, - "endBinding": { - "elementId": "usP9B_26Zn4ILtIt8BOtJ", - "focus": -0.13919775461940126, - "gap": 7 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -30.11325162617777, - 43 - ] - ] - }, - { - "type": "arrow", - "version": 41, - "versionNonce": 2013935903, - "isDeleted": false, - "id": "qvKn8boKDYANLHE5fhjMf", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3731, - "y": 567, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 142, - "height": 121, - "seed": 12046516, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "startBinding": { - "elementId": "usP9B_26Zn4ILtIt8BOtJ", - "focus": 0.3206837417713116, - "gap": 5 - }, - "endBinding": { - "elementId": "W_Z0rPIVWqCrP39-hy4mg", - "focus": -0.28302515408962187, - "gap": 4 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 36, - 79 - ], - [ - 142, - 121 - ] - ] - }, - { - "type": "ellipse", - "version": 64, - "versionNonce": 262712401, - "isDeleted": false, - "id": "dgte-SOYeYi4zz0QZ9dD7", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3631, - "y": 165, - "strokeColor": "#2f9e44", - "backgroundColor": "transparent", - "width": 442, - "height": 563, - "seed": 2085862156, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 99, - "versionNonce": 499367231, - "isDeleted": false, - "id": "4zMhJnPtF7GwYno5JMIgG", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3918.324996948242, - "y": 310, - "strokeColor": "#2f9e44", - "backgroundColor": "transparent", - "width": 137.35000610351562, - "height": 70, - "seed": 159851700, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Request\ngo routine", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Request\ngo routine", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "text", - "version": 62, - "versionNonce": 1599142449, - "isDeleted": false, - "id": "7I_bJoYteCN5qPVOhyXgX", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4253.858337402344, - "y": 1273, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 352.2833251953125, - "height": 45, - "seed": 1298156864, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Module dependencies", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Module dependencies", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 89, - "versionNonce": 905428319, - "isDeleted": false, - "id": "RfurZjpt5TcKjx-VZezzA", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6225, - "y": 1478, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 664121024, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "Wpz73c7dhL3X0zf1veLZR" - }, - { - "id": "E2PQ-9ixVM_gYvfQ_rg6B", - "type": "arrow" - }, - { - "id": "FfKU25U_LhZsUW53i_Ymq", - "type": "arrow" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 85, - "versionNonce": 343182353, - "isDeleted": false, - "id": "Wpz73c7dhL3X0zf1veLZR", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6330.936437197877, - "y": 1560.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 92.16666412353516, - "height": 45, - "seed": 1377769152, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "query", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "RfurZjpt5TcKjx-VZezzA", - "originalText": "query", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 194, - "versionNonce": 66500991, - "isDeleted": false, - "id": "Lm2bHPoszTRmmUbvoLxIA", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 7120, - "y": 1464, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1348060480, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "AKVDzCpZA4uo93lq0VLfW" - }, - { - "id": "n2Hr2TdfICByY0hGwgQgr", - "type": "arrow" - }, - { - "id": "h4_nmiMI5Cv2xJdsvSKxa", - "type": "arrow" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 198, - "versionNonce": 1728766449, - "isDeleted": false, - "id": "AKVDzCpZA4uo93lq0VLfW", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 7212.544770785524, - "y": 1546.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 118.94999694824219, - "height": 45, - "seed": 1131355456, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "routing", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "Lm2bHPoszTRmmUbvoLxIA", - "originalText": "routing", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 135, - "versionNonce": 218182047, - "isDeleted": false, - "id": "AMFC3PK6iWufiVTfqbNHt", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4947, - "y": 1631, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 126092992, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "K1VGoPcRi78m6ljIa7b-g" - }, - { - "id": "kQY6G1aGsMUSAd7QowH2S", - "type": "arrow" - }, - { - "id": "hNOGPfZDtPF9phvYhGi3m", - "type": "arrow" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 146, - "versionNonce": 605780945, - "isDeleted": false, - "id": "K1VGoPcRi78m6ljIa7b-g", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5030.544770785524, - "y": 1690.7537879754125, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 136.9499969482422, - "height": 90, - "seed": 1859464896, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "routing \ntable", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "AMFC3PK6iWufiVTfqbNHt", - "originalText": "routing table", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 137, - "versionNonce": 1514644927, - "isDeleted": false, - "id": "-SZTbrk33cCzKPjwsLKcb", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5434, - "y": 2099, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 2057973440, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "L8oqvWXq7mNoEtyS7wnmf" - }, - { - "id": "oo0JR6g9FYhHmskGKvAbW", - "type": "arrow" - }, - { - "id": "bpx2IFahT3lMU5DZui40c", - "type": "arrow" - }, - { - "id": "t1WTqcxmqLrn_TqxNh8H3", - "type": "arrow" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 183, - "versionNonce": 196991409, - "isDeleted": false, - "id": "L8oqvWXq7mNoEtyS7wnmf", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5516.153100558473, - "y": 2158.7537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 139.73333740234375, - "height": 90, - "seed": 1794528960, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "libp2p \nendpoint", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "-SZTbrk33cCzKPjwsLKcb", - "originalText": "libp2p endpoint", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 179, - "versionNonce": 2062194143, - "isDeleted": false, - "id": "g3PatidDBrRR1z2eRTe1d", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5457, - "y": 2404, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1665068736, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "gX9dl5oXzKGtpuq7FcVhE" - }, - { - "id": "goBvGX8v3jzWhgJU4EHSt", - "type": "arrow" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 234, - "versionNonce": 1863941009, - "isDeleted": false, - "id": "gX9dl5oXzKGtpuq7FcVhE", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5539.153100558473, - "y": 2463.7537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 139.73333740234375, - "height": 90, - "seed": 1660098240, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "fake \nendpoint", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "g3PatidDBrRR1z2eRTe1d", - "originalText": "fake endpoint", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 181, - "versionNonce": 1174045183, - "isDeleted": false, - "id": "-iRuuHQF7CLwPOtG_8MHP", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6411, - "y": 2000, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 519259456, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "qwklbRnmg1k73ZuC9piJp" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 234, - "versionNonce": 241557873, - "isDeleted": false, - "id": "qwklbRnmg1k73ZuC9piJp", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6535.9614368164075, - "y": 2082.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 54.11666488647461, - "height": 45, - "seed": 47750464, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "key", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "-iRuuHQF7CLwPOtG_8MHP", - "originalText": "key", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 169, - "versionNonce": 1551362591, - "isDeleted": false, - "id": "4L2KoxaHpImCvRVYTu81K", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5905, - "y": 1819, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 23455040, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "qXqyc4Nezts_n_Lgf3Jnq" - }, - { - "id": "bpx2IFahT3lMU5DZui40c", - "type": "arrow" - }, - { - "id": "M6E8UKc_5yRRp_qL9P9dn", - "type": "arrow" - }, - { - "id": "1qlCSaqrW84jkFvqT4xL1", - "type": "arrow" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 223, - "versionNonce": 921340753, - "isDeleted": false, - "id": "qXqyc4Nezts_n_Lgf3Jnq", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6006.519769259645, - "y": 1901.2537879754125, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 101, - "height": 45, - "seed": 1039613248, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "peerid", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "4L2KoxaHpImCvRVYTu81K", - "originalText": "peerid", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 159, - "versionNonce": 1800621631, - "isDeleted": false, - "id": "g0O8SNyutykZMZ2Kxc_S6", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5902, - "y": 2067, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 653299392, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "NSLbwyE-SyKQ69Q0EkjO5" - }, - { - "id": "oo0JR6g9FYhHmskGKvAbW", - "type": "arrow" - } - ], - "updated": 1687761516830, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 222, - "versionNonce": 553580849, - "isDeleted": false, - "id": "NSLbwyE-SyKQ69Q0EkjO5", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6005.636434146119, - "y": 2149.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 96.76667022705078, - "height": 45, - "seed": 2138093248, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516830, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "libp2p", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "g0O8SNyutykZMZ2Kxc_S6", - "originalText": "libp2p", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "arrow", - "version": 292, - "versionNonce": 762130015, - "isDeleted": false, - "id": "oo0JR6g9FYhHmskGKvAbW", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5744.739213605669, - "y": 2168.365131955409, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 147.3485189607909, - "height": 7.067048345812964, - "seed": 1088769728, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "startBinding": { - "elementId": "-SZTbrk33cCzKPjwsLKcb", - "focus": -0.40215319782025205, - "gap": 14.328838363837406 - }, - "endBinding": { - "elementId": "g0O8SNyutykZMZ2Kxc_S6", - "focus": -0.10638890392714413, - "gap": 9.984444975307298 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 147.3485189607909, - 7.067048345812964 - ] - ] - }, - { - "type": "arrow", - "version": 282, - "versionNonce": 235580177, - "isDeleted": false, - "id": "bpx2IFahT3lMU5DZui40c", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5741.355279921232, - "y": 2155.488075379929, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 187.12646394568765, - "height": 164.81225819918018, - "seed": 2081189184, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "startBinding": { - "elementId": "-SZTbrk33cCzKPjwsLKcb", - "focus": 0.5211779184495863, - "gap": 17.25148261549745 - }, - "endBinding": { - "elementId": "4L2KoxaHpImCvRVYTu81K", - "focus": 0.2734057579211033, - "gap": 7.535248799106938 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 187.12646394568765, - -164.81225819918018 - ] - ] - }, - { - "type": "ellipse", - "version": 133, - "versionNonce": 764807807, - "isDeleted": false, - "id": "CLKelk42SWChwwQKepZNC", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4965, - "y": 2055, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1688098496, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "vvvgI3nuD6718uN1jtJOE" - }, - { - "id": "t1WTqcxmqLrn_TqxNh8H3", - "type": "arrow" - }, - { - "id": "goBvGX8v3jzWhgJU4EHSt", - "type": "arrow" - }, - { - "id": "VO6HMvB0S3uFj1hVVQlj2", - "type": "arrow" - } - ], - "updated": 1687761516831, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 193, - "versionNonce": 646258929, - "isDeleted": false, - "id": "vvvgI3nuD6718uN1jtJOE", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5047.153100558473, - "y": 2137.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 139.73333740234375, - "height": 45, - "seed": 1411743424, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "endpoint", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "CLKelk42SWChwwQKepZNC", - "originalText": "endpoint", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "arrow", - "version": 143, - "versionNonce": 1217290911, - "isDeleted": false, - "id": "t1WTqcxmqLrn_TqxNh8H3", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5289.285427270407, - "y": 2158.283618973338, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 135.03684023490314, - "height": 35.69244360312268, - "seed": 1432204608, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "startBinding": { - "elementId": "CLKelk42SWChwwQKepZNC", - "focus": -0.42032222681587644, - "gap": 20.30129487679423 - }, - "endBinding": { - "elementId": "-SZTbrk33cCzKPjwsLKcb", - "focus": -0.29095369854186914, - "gap": 10.285565176537006 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 135.03684023490314, - 35.69244360312268 - ] - ] - }, - { - "type": "arrow", - "version": 127, - "versionNonce": 401988305, - "isDeleted": false, - "id": "goBvGX8v3jzWhgJU4EHSt", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5283.969882006496, - "y": 2177.5581908099707, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 248.58328757398067, - "height": 227.86861922938442, - "seed": 1427199680, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "startBinding": { - "elementId": "CLKelk42SWChwwQKepZNC", - "focus": -0.7766374972026974, - "gap": 16.7069027809072 - }, - "endBinding": { - "elementId": "g3PatidDBrRR1z2eRTe1d", - "focus": 0.19199425494050495, - "gap": 11.954237420832214 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 248.58328757398067, - 227.86861922938442 - ] - ] - }, - { - "type": "ellipse", - "version": 166, - "versionNonce": 1862710975, - "isDeleted": false, - "id": "rnvW1p_iD59IRxklsnRKG", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5405, - "y": 1626, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1745950016, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "PUuNlns-UMSQL6tX3FYn2" - }, - { - "id": "kQY6G1aGsMUSAd7QowH2S", - "type": "arrow" - }, - { - "id": "M6E8UKc_5yRRp_qL9P9dn", - "type": "arrow" - } - ], - "updated": 1687761516831, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 201, - "versionNonce": 1928616113, - "isDeleted": false, - "id": "PUuNlns-UMSQL6tX3FYn2", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5488.544770785524, - "y": 1663.2537879754125, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 136.9499969482422, - "height": 135, - "seed": 129789248, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "libp2p \nrouting \ntable", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "rnvW1p_iD59IRxklsnRKG", - "originalText": "libp2p routing table", - "lineHeight": 1.25, - "baseline": 122 - }, - { - "type": "arrow", - "version": 68, - "versionNonce": 664766175, - "isDeleted": false, - "id": "kQY6G1aGsMUSAd7QowH2S", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5262, - "y": 1726, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 130, - "height": 0, - "seed": 723400000, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "startBinding": { - "elementId": "AMFC3PK6iWufiVTfqbNHt", - "focus": -0.09523809523809523, - "gap": 11.595486822154697 - }, - "endBinding": { - "elementId": "rnvW1p_iD59IRxklsnRKG", - "focus": 0.047619047619047616, - "gap": 13.145962930851681 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 130, - 0 - ] - ] - }, - { - "type": "arrow", - "version": 83, - "versionNonce": 1227365009, - "isDeleted": false, - "id": "M6E8UKc_5yRRp_qL9P9dn", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5716.9938237602, - "y": 1758.3529073703428, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 206.42900775957423, - "height": 102.48629945059224, - "seed": 762033472, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "startBinding": { - "elementId": "rnvW1p_iD59IRxklsnRKG", - "focus": -0.38800199980274597, - "gap": 12.512522824356552 - }, - "endBinding": { - "elementId": "4L2KoxaHpImCvRVYTu81K", - "focus": -0.02441189822172934, - "gap": 8.665255399971784 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 206.42900775957423, - 102.48629945059224 - ] - ] - }, - { - "type": "ellipse", - "version": 177, - "versionNonce": 1357468415, - "isDeleted": false, - "id": "5itrQw3zwm4eJMr1fyfrI", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5832, - "y": 1320, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 128181568, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "iByqbBY14iTabI7VYSG9T" - }, - { - "id": "XhlRvImiEp3Amj9mHC7-P", - "type": "arrow" - } - ], - "updated": 1687761516831, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 249, - "versionNonce": 189309041, - "isDeleted": false, - "id": "iByqbBY14iTabI7VYSG9T", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5893.019769259645, - "y": 1379.7537879754125, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 182, - "height": 90, - "seed": 1282751808, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "other \naddressing", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "5itrQw3zwm4eJMr1fyfrI", - "originalText": "other addressing", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 184, - "versionNonce": 593795871, - "isDeleted": false, - "id": "CRvMlacWi9qvfdcvxoRQB", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5406, - "y": 1357, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 157804224, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "JXWAaYFciEuV4d1pTcmdE" - }, - { - "id": "XhlRvImiEp3Amj9mHC7-P", - "type": "arrow" - }, - { - "id": "hNOGPfZDtPF9phvYhGi3m", - "type": "arrow" - } - ], - "updated": 1687761516831, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 224, - "versionNonce": 656239185, - "isDeleted": false, - "id": "JXWAaYFciEuV4d1pTcmdE", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5464.844766207887, - "y": 1394.2537879754125, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 186.35000610351562, - "height": 135, - "seed": 1228738240, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "non libp2p \nrouting \ntable", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "CRvMlacWi9qvfdcvxoRQB", - "originalText": "non libp2p routing table", - "lineHeight": 1.25, - "baseline": 122 - }, - { - "type": "ellipse", - "version": 164, - "versionNonce": 1937891135, - "isDeleted": false, - "id": "9rtfOhNDGVRawLHBC0OyV", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5422, - "y": 1875, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1847959232, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "grcpQpUSH55H0hLETYMSX" - }, - { - "id": "VO6HMvB0S3uFj1hVVQlj2", - "type": "arrow" - } - ], - "updated": 1687761516831, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 216, - "versionNonce": 1702754353, - "isDeleted": false, - "id": "grcpQpUSH55H0hLETYMSX", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5504.153100558473, - "y": 1934.7537879754125, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 139.73333740234375, - "height": 90, - "seed": 1032046272, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "other \nendpoint", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "9rtfOhNDGVRawLHBC0OyV", - "originalText": "other endpoint", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "arrow", - "version": 31, - "versionNonce": 866230111, - "isDeleted": false, - "id": "VO6HMvB0S3uFj1hVVQlj2", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5284, - "y": 2128, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 138, - "height": 95, - "seed": 1089830208, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "startBinding": { - "elementId": "CLKelk42SWChwwQKepZNC", - "focus": 0.559671859435529, - "gap": 20.596941313466402 - }, - "endBinding": { - "elementId": "9rtfOhNDGVRawLHBC0OyV", - "focus": 0.34834672638113195, - "gap": 16.63230771618518 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 138, - -95 - ] - ] - }, - { - "type": "arrow", - "version": 44, - "versionNonce": 139577873, - "isDeleted": false, - "id": "2RtEspMadOPnN-0aaI8bc", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5714, - "y": 1918, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 221, - "height": 380, - "seed": 1770071744, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516831, - "link": null, - "locked": false, - "startBinding": null, - "endBinding": null, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 221, - -380 - ] - ] - }, - { - "type": "arrow", - "version": 20, - "versionNonce": 907303807, - "isDeleted": false, - "id": "XhlRvImiEp3Amj9mHC7-P", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5716, - "y": 1459, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 111, - "height": 8, - "seed": 610696512, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "CRvMlacWi9qvfdcvxoRQB", - "focus": 0.07944863695572135, - "gap": 6.057269871275992 - }, - "endBinding": { - "elementId": "5itrQw3zwm4eJMr1fyfrI", - "focus": -0.13909911733386232, - "gap": 9.193277072217143 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 111, - -8 - ] - ] - }, - { - "type": "arrow", - "version": 36, - "versionNonce": 680550385, - "isDeleted": false, - "id": "hNOGPfZDtPF9phvYhGi3m", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5235, - "y": 1668, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 203, - "height": 126, - "seed": 1178078912, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "AMFC3PK6iWufiVTfqbNHt", - "focus": 0.11627851452049758, - "gap": 13.632065432500923 - }, - "endBinding": { - "elementId": "CRvMlacWi9qvfdcvxoRQB", - "focus": -0.03908521496487313, - "gap": 12.015606009441356 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 203, - -126 - ] - ] - }, - { - "type": "ellipse", - "version": 152, - "versionNonce": 790270879, - "isDeleted": false, - "id": "YP3zmRwTBJ0qn62aaVoTl", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6604, - "y": 1587, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1311798592, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "5B8jgeGyTFKUrtdAwH8nA" - }, - { - "id": "1qlCSaqrW84jkFvqT4xL1", - "type": "arrow" - }, - { - "id": "FfKU25U_LhZsUW53i_Ymq", - "type": "arrow" - }, - { - "id": "h4_nmiMI5Cv2xJdsvSKxa", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 162, - "versionNonce": 1413560785, - "isDeleted": false, - "id": "5B8jgeGyTFKUrtdAwH8nA", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6696.544770785524, - "y": 1646.7537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 118.94999694824219, - "height": 90, - "seed": 854576448, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "IPFS \nrouting", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "YP3zmRwTBJ0qn62aaVoTl", - "originalText": "IPFS routing", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 224, - "versionNonce": 1994704831, - "isDeleted": false, - "id": "CgnOyVzJawqeimh2qxHRo", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6586, - "y": 1332, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1046764864, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "uSD1hYuFHsuV3nXxVkbS3" - }, - { - "id": "E2PQ-9ixVM_gYvfQ_rg6B", - "type": "arrow" - }, - { - "id": "n2Hr2TdfICByY0hGwgQgr", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 242, - "versionNonce": 1820055473, - "isDeleted": false, - "id": "uSD1hYuFHsuV3nXxVkbS3", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6678.544770785524, - "y": 1391.7537879754125, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 118.94999694824219, - "height": 90, - "seed": 1147581760, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "other \nrouting", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "CgnOyVzJawqeimh2qxHRo", - "originalText": "other routing", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "arrow", - "version": 46, - "versionNonce": 194213855, - "isDeleted": false, - "id": "0ycYIZKmJPCKGauQcKLoI", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6568, - "y": 1436, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 420, - "height": 6, - "seed": 1159214784, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": null, - "endBinding": null, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -420, - -6 - ] - ] - }, - { - "type": "arrow", - "version": 25, - "versionNonce": 1130871185, - "isDeleted": false, - "id": "1qlCSaqrW84jkFvqT4xL1", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6597, - "y": 1716, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 381, - "height": 176, - "seed": 1140729152, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "YP3zmRwTBJ0qn62aaVoTl", - "focus": 0.39147596381229727, - "gap": 10.50833230848994 - }, - "endBinding": { - "elementId": "4L2KoxaHpImCvRVYTu81K", - "focus": 0.32814163590890655, - "gap": 13.099551717764257 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -381, - 176 - ] - ] - }, - { - "type": "arrow", - "version": 55, - "versionNonce": 1396122623, - "isDeleted": false, - "id": "E2PQ-9ixVM_gYvfQ_rg6B", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6608, - "y": 1505, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 78, - "height": 39, - "seed": 602683072, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "CgnOyVzJawqeimh2qxHRo", - "focus": -0.02314481367082378, - "gap": 9.398984084117728 - }, - "endBinding": { - "elementId": "RfurZjpt5TcKjx-VZezzA", - "focus": 0.2893101708852972, - "gap": 10.454771700657062 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -78, - 39 - ] - ] - }, - { - "type": "arrow", - "version": 24, - "versionNonce": 2050776945, - "isDeleted": false, - "id": "FfKU25U_LhZsUW53i_Ymq", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6607, - "y": 1646, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 73, - "height": 33, - "seed": 984558912, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "YP3zmRwTBJ0qn62aaVoTl", - "focus": -0.17018952817133345, - "gap": 10.343957335277082 - }, - "endBinding": { - "elementId": "RfurZjpt5TcKjx-VZezzA", - "focus": -0.32651499599772826, - "gap": 10.51763314060048 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -73, - -33 - ] - ] - }, - { - "type": "arrow", - "version": 36, - "versionNonce": 500190239, - "isDeleted": false, - "id": "n2Hr2TdfICByY0hGwgQgr", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 7115, - "y": 1544, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 222, - "height": 70, - "seed": 2042770752, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "Lm2bHPoszTRmmUbvoLxIA", - "focus": -0.21230479563353097, - "gap": 8.887571390378582 - }, - "endBinding": { - "elementId": "CgnOyVzJawqeimh2qxHRo", - "focus": -0.10287416200183595, - "gap": 11.385125690849975 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -222, - -70 - ] - ] - }, - { - "type": "arrow", - "version": 26, - "versionNonce": 524372305, - "isDeleted": false, - "id": "h4_nmiMI5Cv2xJdsvSKxa", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 7126, - "y": 1620, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 212, - "height": 50, - "seed": 873444672, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "Lm2bHPoszTRmmUbvoLxIA", - "focus": -0.14930930988986554, - "gap": 10.540964993134537 - }, - "endBinding": { - "elementId": "YP3zmRwTBJ0qn62aaVoTl", - "focus": 0.13757543473906744, - "gap": 8.997636175635535 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -212, - 50 - ] - ] - }, - { - "type": "ellipse", - "version": 344, - "versionNonce": 1733259327, - "isDeleted": false, - "id": "RpGtGnAEM5O305ue70Str", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5100, - "y": 3212, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1311059264, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "tcInCKVq7M1wab0bKX-PL" - }, - { - "id": "hUcIFgDhF0CgzUgT_3GpJ", - "type": "arrow" - }, - { - "id": "LtbzYN46nlXRGwlUJhjYg", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 347, - "versionNonce": 198047537, - "isDeleted": false, - "id": "tcInCKVq7M1wab0bKX-PL", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5192.544770785524, - "y": 3294.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 118.94999694824219, - "height": 45, - "seed": 1133348160, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "routing", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "RpGtGnAEM5O305ue70Str", - "originalText": "routing", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 222, - "versionNonce": 1502941279, - "isDeleted": false, - "id": "GJHwElIp_zEXsTMsA_BTT", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5805, - "y": 3335, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 830774592, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "XhN-EMjnFtbJcIxrsSzC0" - }, - { - "id": "QFXnLAJ7483Y0_b59uLRk", - "type": "arrow" - }, - { - "id": "yIG57fs7mFTt7rmbQGBJx", - "type": "arrow" - }, - { - "id": "Wjl47FSd_M9D17RP776er", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 279, - "versionNonce": 38398225, - "isDeleted": false, - "id": "XhN-EMjnFtbJcIxrsSzC0", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5887.153100558473, - "y": 3417.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 139.73333740234375, - "height": 45, - "seed": 1769928000, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "endpoint", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "GJHwElIp_zEXsTMsA_BTT", - "originalText": "endpoint", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 288, - "versionNonce": 242330751, - "isDeleted": false, - "id": "P5ENqnHgKaSAsEM7IANlK", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6241, - "y": 2852, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 2050694848, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "BdIZPH-Zp1Vka9ZTtkcMg" - }, - { - "id": "KVlBvI-rCoizyABQw22JY", - "type": "arrow" - }, - { - "id": "PQhJUMt_4rnc6NO4sGb0J", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 339, - "versionNonce": 204250865, - "isDeleted": false, - "id": "BdIZPH-Zp1Vka9ZTtkcMg", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6365.9614368164075, - "y": 2934.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 54.11666488647461, - "height": 45, - "seed": 719931072, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "key", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "P5ENqnHgKaSAsEM7IANlK", - "originalText": "key", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 274, - "versionNonce": 296076447, - "isDeleted": false, - "id": "5bbRqaDayOG8Lgskte1_V", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6257, - "y": 3118, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 2023767744, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "R_VhPKSeRZEk74b6Bq0y4" - }, - { - "id": "QFXnLAJ7483Y0_b59uLRk", - "type": "arrow" - }, - { - "id": "2HziBiOiO7FPAOji90TIf", - "type": "arrow" - }, - { - "id": "LtbzYN46nlXRGwlUJhjYg", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 343, - "versionNonce": 402495697, - "isDeleted": false, - "id": "R_VhPKSeRZEk74b6Bq0y4", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6309.019769259645, - "y": 3177.7537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 200, - "height": 90, - "seed": 1605002944, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "addressing \nscheme", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "5bbRqaDayOG8Lgskte1_V", - "originalText": "addressing scheme", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 215, - "versionNonce": 585226431, - "isDeleted": false, - "id": "hrfQsD4D0fI-E0HjlXl5z", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5464, - "y": 2876, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1593366208, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "f5K2msqXp5VzNKetGKDCI" - }, - { - "id": "hUcIFgDhF0CgzUgT_3GpJ", - "type": "arrow" - }, - { - "id": "Wjl47FSd_M9D17RP776er", - "type": "arrow" - }, - { - "id": "jntFh70OMtXJegisfUqf4", - "type": "arrow" - }, - { - "id": "KVlBvI-rCoizyABQw22JY", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 208, - "versionNonce": 1465145009, - "isDeleted": false, - "id": "f5K2msqXp5VzNKetGKDCI", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5569.936437197877, - "y": 2958.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 92.16666412353516, - "height": 45, - "seed": 523239104, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "query", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "hrfQsD4D0fI-E0HjlXl5z", - "originalText": "query", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 328, - "versionNonce": 935749855, - "isDeleted": false, - "id": "5PcXBVbD0-dvS-6N6zozH", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5842, - "y": 3047, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1529810624, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "6if7HlhuBKN1Y6oOZbz3D" - }, - { - "id": "jntFh70OMtXJegisfUqf4", - "type": "arrow" - }, - { - "id": "PQhJUMt_4rnc6NO4sGb0J", - "type": "arrow" - }, - { - "id": "2HziBiOiO7FPAOji90TIf", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 337, - "versionNonce": 1557682321, - "isDeleted": false, - "id": "6if7HlhuBKN1Y6oOZbz3D", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5925.544770785524, - "y": 3106.7537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 136.9499969482422, - "height": 90, - "seed": 668939968, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "routing \ntable", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "5PcXBVbD0-dvS-6N6zozH", - "originalText": "routing table", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 295, - "versionNonce": 1038349567, - "isDeleted": false, - "id": "wmdHrCpP3nn9LeTzdLO1s", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6255, - "y": 3380, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 304, - "height": 210, - "seed": 1840983744, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "7_37E6DXnGehGK4MEKfQI" - }, - { - "id": "yIG57fs7mFTt7rmbQGBJx", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 376, - "versionNonce": 2051087985, - "isDeleted": false, - "id": "7_37E6DXnGehGK4MEKfQI", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6322.161439486696, - "y": 3462.2537879754127, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 169.71665954589844, - "height": 45, - "seed": 1407669952, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "transport", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "wmdHrCpP3nn9LeTzdLO1s", - "originalText": "transport", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "arrow", - "version": 224, - "versionNonce": 482016543, - "isDeleted": false, - "id": "QFXnLAJ7483Y0_b59uLRk", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6104.75109525845, - "y": 3392.4747070498224, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 168.90575771949625, - "height": 104.95900857107426, - "seed": 530568512, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "GJHwElIp_zEXsTMsA_BTT", - "focus": 0.31778701048560193, - "gap": 10.19757008285194 - }, - "endBinding": { - "elementId": "5bbRqaDayOG8Lgskte1_V", - "focus": 0.1386888366012517, - "gap": 10.910632759350335 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 168.90575771949625, - -104.95900857107426 - ] - ] - }, - { - "type": "arrow", - "version": 214, - "versionNonce": 1582704721, - "isDeleted": false, - "id": "yIG57fs7mFTt7rmbQGBJx", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6114.909084732304, - "y": 3414.2996865987616, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 135.8437444850597, - "height": 37.96098411748699, - "seed": 92360000, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "GJHwElIp_zEXsTMsA_BTT", - "focus": -0.6078129374745338, - "gap": 10.006057866050298 - }, - "endBinding": { - "elementId": "wmdHrCpP3nn9LeTzdLO1s", - "focus": -0.0964394198005009, - "gap": 10.882833999373304 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 135.8437444850597, - 37.96098411748699 - ] - ] - }, - { - "type": "arrow", - "version": 144, - "versionNonce": 2008809791, - "isDeleted": false, - "id": "hUcIFgDhF0CgzUgT_3GpJ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5318.823475951995, - "y": 3207.5775486969997, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 192.1966485289613, - "height": 139.59119405697493, - "seed": 1890655936, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "RpGtGnAEM5O305ue70Str", - "focus": -0.39580079142736335, - "gap": 14.470285675293894 - }, - "endBinding": { - "elementId": "hrfQsD4D0fI-E0HjlXl5z", - "focus": -0.07049363982758232, - "gap": 9.479937258022659 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 192.1966485289613, - -139.59119405697493 - ] - ] - }, - { - "type": "arrow", - "version": 97, - "versionNonce": 1928263217, - "isDeleted": false, - "id": "Wjl47FSd_M9D17RP776er", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5700.940733254742, - "y": 3078.931106701709, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 183.8393194857308, - "height": 260.86641484062466, - "seed": 1811535552, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "hrfQsD4D0fI-E0HjlXl5z", - "focus": -0.09653181731835807, - "gap": 10.006057866050298 - }, - "endBinding": { - "elementId": "GJHwElIp_zEXsTMsA_BTT", - "focus": -0.009491996879170647, - "gap": 7.400859681877506 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 183.8393194857308, - 260.86641484062466 - ] - ] - }, - { - "type": "arrow", - "version": 186, - "versionNonce": 809992543, - "isDeleted": false, - "id": "jntFh70OMtXJegisfUqf4", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5756.859840135434, - "y": 3037.0105110867116, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 101.28666061499825, - "height": 51.244025010077166, - "seed": 1632673472, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "hrfQsD4D0fI-E0HjlXl5z", - "focus": -0.10309487545936809, - "gap": 9.50145789237746 - }, - "endBinding": { - "elementId": "5PcXBVbD0-dvS-6N6zozH", - "focus": -0.0383174154681844, - "gap": 10.755458326069416 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 101.28666061499825, - 51.244025010077166 - ] - ] - }, - { - "type": "arrow", - "version": 101, - "versionNonce": 2062747665, - "isDeleted": false, - "id": "KVlBvI-rCoizyABQw22JY", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5776.999823701711, - "y": 2951.4984059145017, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 462.23063176595224, - "height": 34.75781618738165, - "seed": 1761291584, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "hrfQsD4D0fI-E0HjlXl5z", - "focus": -0.1679046554310248, - "gap": 14.172102911244025 - }, - "endBinding": { - "elementId": "P5ENqnHgKaSAsEM7IANlK", - "focus": 0.49064675963373033, - "gap": 11.806784919486034 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 462.23063176595224, - -34.75781618738165 - ] - ] - }, - { - "type": "arrow", - "version": 104, - "versionNonce": 665714047, - "isDeleted": false, - "id": "PQhJUMt_4rnc6NO4sGb0J", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6123.682179270349, - "y": 3067.0426862291224, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 135.0891352622839, - "height": 45.13755930038951, - "seed": 116205248, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "5PcXBVbD0-dvS-6N6zozH", - "focus": -0.3540937012181681, - "gap": 22.221843321883796 - }, - "endBinding": { - "elementId": "P5ENqnHgKaSAsEM7IANlK", - "focus": -0.17194286248070925, - "gap": 10.363196462645988 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 135.0891352622839, - -45.13755930038951 - ] - ] - }, - { - "type": "arrow", - "version": 95, - "versionNonce": 1884937713, - "isDeleted": false, - "id": "2HziBiOiO7FPAOji90TIf", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6155.624885964104, - "y": 3165.8714962905287, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 95.5884997909152, - "height": 48.11130010895158, - "seed": 1355472576, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "5PcXBVbD0-dvS-6N6zozH", - "focus": -0.5120131396277277, - "gap": 10.797173229617414 - }, - "endBinding": { - "elementId": "5bbRqaDayOG8Lgskte1_V", - "focus": -0.5418889122789331, - "gap": 6.306949793369057 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 95.5884997909152, - 48.11130010895158 - ] - ] - }, - { - "type": "arrow", - "version": 103, - "versionNonce": 1093662111, - "isDeleted": false, - "id": "LtbzYN46nlXRGwlUJhjYg", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5413.917688482175, - "y": 3321.819873627258, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 840.831083559412, - "height": 61.14272177298835, - "seed": 1154139840, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "startBinding": { - "elementId": "RpGtGnAEM5O305ue70Str", - "focus": 0.15770004693222975, - "gap": 10.059940796071714 - }, - "endBinding": { - "elementId": "5bbRqaDayOG8Lgskte1_V", - "focus": -0.25061967971478044, - "gap": 11.080561934242098 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 840.831083559412, - -61.14272177298835 - ] - ] - }, - { - "type": "text", - "version": 168, - "versionNonce": 573241297, - "isDeleted": false, - "id": "b8E9sjWHOiPeKihompPHw", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6718.791656494141, - "y": 3024, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 642.4166870117188, - "height": 70, - "seed": 347780416, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "translation from peerid to kadkey is impossible\nthe mapping should be stored somewhere", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "translation from peerid to kadkey is impossible\nthe mapping should be stored somewhere", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "ellipse", - "version": 127, - "versionNonce": 603843007, - "isDeleted": false, - "id": "JfhvLxAktfzLpSrHvDymY", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6077.5, - "y": 4853.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 347, - "height": 311, - "seed": 270574255, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "hrqfe7wQEBdDiFYDSF1Vj", - "type": "arrow" - }, - { - "id": "jAaUZEA11b9qtkwcJLOYU", - "type": "arrow" - }, - { - "id": "Sr8K1w7wI1SX6P25iYtzm", - "type": "arrow" - }, - { - "id": "wgxiJme0myN8T_8EZXf_r", - "type": "arrow" - }, - { - "id": "Mdd9u3nwmmAS_-eOZKLjQ", - "type": "arrow" - }, - { - "id": "-bxZXBPiQ599iYyv5FANy", - "type": "arrow" - }, - { - "id": "yWmeGBBmex9QGeAugNfIo", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "ellipse", - "version": 75, - "versionNonce": 310236593, - "isDeleted": false, - "id": "nkwVx6ezGcxnnBWJxeYP7", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6695.5, - "y": 5043.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 368, - "height": 191, - "seed": 1160630159, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "5MSV6I-tCFekqBYMU7xUx" - }, - { - "id": "jAaUZEA11b9qtkwcJLOYU", - "type": "arrow" - }, - { - "id": "PyQhTlVQEv4H4HcY1fjbz", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 84, - "versionNonce": 1027103199, - "isDeleted": false, - "id": "5MSV6I-tCFekqBYMU7xUx", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6765.017352261675, - "y": 5116.721302396685, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 228.75, - "height": 45, - "seed": 1026103329, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "routing table", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "nkwVx6ezGcxnnBWJxeYP7", - "originalText": "routing table", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 65, - "versionNonce": 343537553, - "isDeleted": false, - "id": "XV5Saj0kb--9CW6Ul2xF_", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6679.5, - "y": 4799.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 374, - "height": 191, - "seed": 1383473487, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "QObnCT4rVTfoUs4m73soL" - }, - { - "id": "Sr8K1w7wI1SX6P25iYtzm", - "type": "arrow" - }, - { - "id": "OZ7B18GOUsXNd-i-hHOmb", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 74, - "versionNonce": 32491007, - "isDeleted": false, - "id": "QObnCT4rVTfoUs4m73soL", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6785.604367794581, - "y": 4850.221302396685, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 161.3333282470703, - "height": 90, - "seed": 1264599553, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "message \nendpoint", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "XV5Saj0kb--9CW6Ul2xF_", - "originalText": "message endpoint", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 110, - "versionNonce": 710335857, - "isDeleted": false, - "id": "dyWtaRBAGO2rQofXeTpeO", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6408.5, - "y": 4427.25, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 374, - "height": 191, - "seed": 1310433281, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "Mo_-ogvC73ELKJSVIl1By" - }, - { - "id": "jAtspnfzGGH0uww82j7p3", - "type": "arrow" - }, - { - "id": "-7hV8voEFjgsWtY4p86Fv", - "type": "arrow" - }, - { - "id": "hrqfe7wQEBdDiFYDSF1Vj", - "type": "arrow" - }, - { - "id": "dMWQ7mK6ae3A2077z0TmU", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 128, - "versionNonce": 1898229279, - "isDeleted": false, - "id": "Mo_-ogvC73ELKJSVIl1By", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6514.929364742823, - "y": 4500.221302396685, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 160.68333435058594, - "height": 45, - "seed": 1418328033, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "scheduler", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "dyWtaRBAGO2rQofXeTpeO", - "originalText": "scheduler", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 104, - "versionNonce": 593233745, - "isDeleted": false, - "id": "v854c_UhfRyMBhE8syUfT", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6880.5, - "y": 4375.25, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 374, - "height": 191, - "seed": 100435265, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "wd0syZ-XjvQZwl0Tf25Mk" - }, - { - "id": "-7hV8voEFjgsWtY4p86Fv", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 136, - "versionNonce": 1439142463, - "isDeleted": false, - "id": "wd0syZ-XjvQZwl0Tf25Mk", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6960.604367794581, - "y": 4448.221302396685, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 213.3333282470703, - "height": 45, - "seed": 1100756257, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516832, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "event queue", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "v854c_UhfRyMBhE8syUfT", - "originalText": "event queue", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 130, - "versionNonce": 2044785969, - "isDeleted": false, - "id": "SWXb1cHIYWGh-CPRMlNP2", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6834.5, - "y": 4156.25, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 374, - "height": 191, - "seed": 585030575, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "J4yeYRIEA4vaiEhPkCqaN" - }, - { - "id": "jAtspnfzGGH0uww82j7p3", - "type": "arrow" - } - ], - "updated": 1687761516832, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 171, - "versionNonce": 1220080223, - "isDeleted": false, - "id": "J4yeYRIEA4vaiEhPkCqaN", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6961.246030392237, - "y": 4229.221302396685, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 120.05000305175781, - "height": 45, - "seed": 688229839, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "planner", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "SWXb1cHIYWGh-CPRMlNP2", - "originalText": "planner", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "arrow", - "version": 96, - "versionNonce": 1900881681, - "isDeleted": false, - "id": "jAtspnfzGGH0uww82j7p3", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6744.5, - "y": 4454.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 110, - "height": 136, - "seed": 709529121, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "dyWtaRBAGO2rQofXeTpeO", - "focus": 0.46459891760318445, - "gap": 8.645568119375355 - }, - "endBinding": { - "elementId": "SWXb1cHIYWGh-CPRMlNP2", - "focus": 0.557562311785674, - "gap": 18.072121711496635 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 110, - -136 - ] - ] - }, - { - "type": "arrow", - "version": 87, - "versionNonce": 1925795455, - "isDeleted": false, - "id": "-7hV8voEFjgsWtY4p86Fv", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6796.5, - "y": 4509.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 77, - "height": 18, - "seed": 1529978145, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "dyWtaRBAGO2rQofXeTpeO", - "focus": 0.3235950272678844, - "gap": 15.32429952706687 - }, - "endBinding": { - "elementId": "v854c_UhfRyMBhE8syUfT", - "focus": 0.23184588312085722, - "gap": 10.750533969333674 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 77, - -18 - ] - ] - }, - { - "type": "arrow", - "version": 155, - "versionNonce": 1947737329, - "isDeleted": false, - "id": "hrqfe7wQEBdDiFYDSF1Vj", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6343.986259438643, - "y": 4863.45508980052, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 183.46899451654463, - "height": 239.14296756239764, - "seed": 772723375, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "JfhvLxAktfzLpSrHvDymY", - "focus": -0.08960353943150288, - "gap": 12.74967066400825 - }, - "endBinding": { - "elementId": "dyWtaRBAGO2rQofXeTpeO", - "focus": -0.04915872646055882, - "gap": 12.374497096857084 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 183.46899451654463, - -239.14296756239764 - ] - ] - }, - { - "type": "arrow", - "version": 171, - "versionNonce": 247015071, - "isDeleted": false, - "id": "jAaUZEA11b9qtkwcJLOYU", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6426.594998500297, - "y": 5063.003893113664, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 262.08275316895015, - "height": 63.58472394919772, - "seed": 1202692801, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "JfhvLxAktfzLpSrHvDymY", - "focus": 0.06922702782564705, - "gap": 11.905954114316728 - }, - "endBinding": { - "elementId": "nkwVx6ezGcxnnBWJxeYP7", - "focus": -0.31905800224680375, - "gap": 8.215689575301468 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 262.08275316895015, - 63.58472394919772 - ] - ] - }, - { - "type": "arrow", - "version": 158, - "versionNonce": 835278545, - "isDeleted": false, - "id": "Sr8K1w7wI1SX6P25iYtzm", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6435.681926951833, - "y": 4993.15131398068, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 243.56900317081818, - "height": 65.51586139622214, - "seed": 1999939535, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "JfhvLxAktfzLpSrHvDymY", - "focus": 0.20681944015110482, - "gap": 12.03978015313723 - }, - "endBinding": { - "elementId": "XV5Saj0kb--9CW6Ul2xF_", - "focus": 0.16659142527389428, - "gap": 9.51893221175979 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 243.56900317081818, - -65.51586139622214 - ] - ] - }, - { - "type": "ellipse", - "version": 117, - "versionNonce": 725154495, - "isDeleted": false, - "id": "MwUs0zXZhhXKpqgumGjJ6", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5589.75, - "y": 4211.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 265, - "height": 214, - "seed": 1929965121, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "T9wPaR2cwR76Xd5CDELbe" - }, - { - "id": "PyQhTlVQEv4H4HcY1fjbz", - "type": "arrow" - }, - { - "id": "OZ7B18GOUsXNd-i-hHOmb", - "type": "arrow" - }, - { - "id": "dMWQ7mK6ae3A2077z0TmU", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 95, - "versionNonce": 2096649393, - "isDeleted": false, - "id": "T9wPaR2cwR76Xd5CDELbe", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5666.51668355455, - "y": 4296.089574413039, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 111.08333587646484, - "height": 45, - "seed": 679408175, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Server", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "MwUs0zXZhhXKpqgumGjJ6", - "originalText": "Server", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "arrow", - "version": 201, - "versionNonce": 753486559, - "isDeleted": false, - "id": "PyQhTlVQEv4H4HcY1fjbz", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5817.21805211006, - "y": 4407.400088153151, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 964.933053553229, - "height": 641.7763282297847, - "seed": 2126318863, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "MwUs0zXZhhXKpqgumGjJ6", - "focus": 0.18327325878977327, - "gap": 11.152565949640632 - }, - "endBinding": { - "elementId": "nkwVx6ezGcxnnBWJxeYP7", - "focus": 0.16315609473863088, - "gap": 8.84443543287864 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 964.933053553229, - 641.7763282297847 - ] - ] - }, - { - "type": "arrow", - "version": 214, - "versionNonce": 1766626961, - "isDeleted": false, - "id": "OZ7B18GOUsXNd-i-hHOmb", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5852.344870766634, - "y": 4373.220558963125, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 946.8405650664918, - "height": 428.3752112596594, - "seed": 53514913, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "MwUs0zXZhhXKpqgumGjJ6", - "focus": -0.036575647840837305, - "gap": 13.382926807838572 - }, - "endBinding": { - "elementId": "XV5Saj0kb--9CW6Ul2xF_", - "focus": 0.4953497067114932, - "gap": 4.5357128832297775 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 946.8405650664918, - 428.3752112596594 - ] - ] - }, - { - "type": "arrow", - "version": 229, - "versionNonce": 274372351, - "isDeleted": false, - "id": "dMWQ7mK6ae3A2077z0TmU", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5863.926979238615, - "y": 4340.071471555673, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 534.441880773511, - "height": 175.42038094412328, - "seed": 2109537537, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "MwUs0zXZhhXKpqgumGjJ6", - "focus": -0.2180184364905562, - "gap": 11.517544863410677 - }, - "endBinding": { - "elementId": "dyWtaRBAGO2rQofXeTpeO", - "focus": -0.5060295242168579, - "gap": 10.575628532705053 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 534.441880773511, - 175.42038094412328 - ] - ] - }, - { - "type": "ellipse", - "version": 68, - "versionNonce": 696097905, - "isDeleted": false, - "id": "7NrE6WBMNcQHyfRV5q-Vb", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5428.5, - "y": 4665.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 347, - "height": 219, - "seed": 1377983233, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "BqikH8RIKrHnplu7RxHEW" - }, - { - "id": "Mdd9u3nwmmAS_-eOZKLjQ", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 67, - "versionNonce": 1812815647, - "isDeleted": false, - "id": "BqikH8RIKrHnplu7RxHEW", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5529.550303237083, - "y": 4752.821807460073, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 144.53334045410156, - "height": 45, - "seed": 403174945, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "FindPeer", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "7NrE6WBMNcQHyfRV5q-Vb", - "originalText": "FindPeer", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 71, - "versionNonce": 760407633, - "isDeleted": false, - "id": "8EW_Sobz6QHCSFxk1J6Wx", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5472.5, - "y": 4891.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 352, - "height": 203, - "seed": 178215215, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "ujeEYsHaoWhNSmN_yvKwf" - }, - { - "id": "-bxZXBPiQ599iYyv5FANy", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 65, - "versionNonce": 846340927, - "isDeleted": false, - "id": "ujeEYsHaoWhNSmN_yvKwf", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5566.815876738218, - "y": 4970.978661709565, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 163.46665954589844, - "height": 45, - "seed": 939293295, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "FindProvs", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "8EW_Sobz6QHCSFxk1J6Wx", - "originalText": "FindProvs", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 61, - "versionNonce": 2141451313, - "isDeleted": false, - "id": "ujk1Q0tJQs3F04Pz1NT4r", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5472.5, - "y": 5102.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 346, - "height": 202, - "seed": 2114075759, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "prFnwQRHjjeiNG1aMImoJ" - }, - { - "id": "yWmeGBBmex9QGeAugNfIo", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 60, - "versionNonce": 870933343, - "isDeleted": false, - "id": "prFnwQRHjjeiNG1aMImoJ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5582.6038612053135, - "y": 5181.3322151001585, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 126.13333129882812, - "height": 45, - "seed": 958337345, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Provide", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "ujk1Q0tJQs3F04Pz1NT4r", - "originalText": "Provide", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 61, - "versionNonce": 1683506705, - "isDeleted": false, - "id": "CroCOKgQ_RmDrnwn6Pzrq", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5486.5, - "y": 4482.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 299, - "height": 167, - "seed": 2065233391, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "-yPz8bONGkHSW6y9AKNvi" - }, - { - "id": "wgxiJme0myN8T_8EZXf_r", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 59, - "versionNonce": 1623433087, - "isDeleted": false, - "id": "-yPz8bONGkHSW6y9AKNvi", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5560.279204913783, - "y": 4521.206583770923, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 151.01666259765625, - "height": 90, - "seed": 86169377, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "IPNS \nPut/Get", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "CroCOKgQ_RmDrnwn6Pzrq", - "originalText": "IPNS Put/Get", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "arrow", - "version": 118, - "versionNonce": 912743409, - "isDeleted": false, - "id": "wgxiJme0myN8T_8EZXf_r", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5777.967518165825, - "y": 4602.844354856792, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 367.301524589835, - "height": 270.3407391741357, - "seed": 68319727, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "CroCOKgQ_RmDrnwn6Pzrq", - "focus": -0.49153960018723936, - "gap": 5.867853735613977 - }, - "endBinding": { - "elementId": "JfhvLxAktfzLpSrHvDymY", - "focus": 0.28946677173901997, - "gap": 10.66287687506275 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 367.301524589835, - 270.3407391741357 - ] - ] - }, - { - "type": "arrow", - "version": 125, - "versionNonce": 1262573471, - "isDeleted": false, - "id": "Mdd9u3nwmmAS_-eOZKLjQ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5780.156430930416, - "y": 4809.131815882339, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 324.7484321834727, - "height": 98.66507344566253, - "seed": 1855347439, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "7NrE6WBMNcQHyfRV5q-Vb", - "focus": -0.1665936163233698, - "gap": 11.898453216623324 - }, - "endBinding": { - "elementId": "JfhvLxAktfzLpSrHvDymY", - "focus": 0.3475602824281056, - "gap": 10.834313803954984 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 324.7484321834727, - 98.66507344566253 - ] - ] - }, - { - "type": "arrow", - "version": 120, - "versionNonce": 98088401, - "isDeleted": false, - "id": "-bxZXBPiQ599iYyv5FANy", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5834.217822211436, - "y": 5005.135260282429, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 238.85737743342725, - "height": 32.765045730252496, - "seed": 1999839777, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "8EW_Sobz6QHCSFxk1J6Wx", - "focus": 0.3580974158800518, - "gap": 10.740646808779502 - }, - "endBinding": { - "elementId": "JfhvLxAktfzLpSrHvDymY", - "focus": 0.38958847295083243, - "gap": 9.065532925158493 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 238.85737743342725, - -32.765045730252496 - ] - ] - }, - { - "type": "arrow", - "version": 117, - "versionNonce": 1771244479, - "isDeleted": false, - "id": "yWmeGBBmex9QGeAugNfIo", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5827.119221784235, - "y": 5187.638052903088, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 245.99785855079244, - "height": 144.59370582856263, - "seed": 1753572609, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "startBinding": { - "elementId": "ujk1Q0tJQs3F04Pz1NT4r", - "focus": 0.6324292309712563, - "gap": 10.498936189368152 - }, - "endBinding": { - "elementId": "JfhvLxAktfzLpSrHvDymY", - "focus": 0.38052973088689834, - "gap": 8.292254104070622 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 245.99785855079244, - -144.59370582856263 - ] - ] - }, - { - "type": "rectangle", - "version": 87, - "versionNonce": 1975767985, - "isDeleted": false, - "id": "_sOLeZouxbv_a3wObeLnk", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5345.5, - "y": 4173.75, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 566, - "height": 1162, - "seed": 33526785, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 94, - "versionNonce": 400112607, - "isDeleted": false, - "id": "nBc0t1dNHz8MExNL5svvj", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 5360.170825958252, - "y": 4091.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 316.45001220703125, - "height": 78.74999999999999, - "seed": 2013617697, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 62.99999999999999, - "fontFamily": 1, - "text": "IPFS DHT", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "IPFS DHT", - "lineHeight": 1.25, - "baseline": 55 - }, - { - "type": "text", - "version": 13, - "versionNonce": 1515206033, - "isDeleted": false, - "id": "ELt0G9wjPJGE1i06FPfHp", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 6192.799999237061, - "y": 4985.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 97.9000015258789, - "height": 45, - "seed": 615976783, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Query", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Query", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "id": "0d5J7x0dJNG4D-018hvR0", - "type": "rectangle", - "x": 2086.75, - "y": 2183, - "width": 936.250000000001, - "height": 923.7500000000001, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "seed": 104403295, - "version": 94, - "versionNonce": 1512335359, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "id": "q6PF9aWwatIedTBlEywh5", - "type": "text", - "x": 2420.8752297295464, - "y": 2209.25, - "width": 221.86666870117188, - "height": 61.25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1240254161, - "version": 66, - "versionNonce": 189593407, - "isDeleted": false, - "boundElements": null, - "updated": 1687762594007, - "link": null, - "locked": false, - "text": "Scheduler", - "fontSize": 49, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 43, - "containerId": null, - "originalText": "Scheduler", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "rectangle", - "version": 150, - "versionNonce": 47764575, - "isDeleted": false, - "id": "rDDQqXj8Ohi190IEyyQuk", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2561.75, - "y": 2302.6875, - "strokeColor": "#1e1e1e", - "backgroundColor": "#a5d8ff", - "width": 418.75, - "height": 262.5, - "seed": 1710432689, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "nJnHUIaYOEbFPI4Um_SSP", - "type": "arrow" - }, - { - "id": "v5XNvHAzT8hEPB-xS8Lm-", - "type": "arrow" - }, - { - "id": "JgDS5iqVOqEGYS58f5c2w", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 72, - "versionNonce": 1393845521, - "isDeleted": false, - "id": "bPub-cfYCrYKumYz1NbV6", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2660.25, - "y": 2326.125, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 238, - "height": 45, - "seed": 1667613585, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Event planner", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Event planner", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "text", - "version": 307, - "versionNonce": 1654452351, - "isDeleted": false, - "id": "KQthn7haA4IFxtXCmEPl7", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2584.2333335876465, - "y": 2444.387367296875, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 368.0666809082031, - "height": 70, - "seed": 1105799537, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "* action 45b7, 09:11:01.145\n* action f966, 14:21:44.192", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "* action 45b7, 09:11:01.145\n* action f966, 14:21:44.192", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "id": "DL1gyY_9x_rKNazOi8w4q", - "type": "diamond", - "x": 2183, - "y": 3125.8125, - "width": 172.5, - "height": 178.75, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [ - "EqchHnvxW4F3CZlxS3ZLf" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 246248849, - "version": 175, - "versionNonce": 1447533297, - "isDeleted": false, - "boundElements": [ - { - "id": "TqgUZf3Ae5kEF3T0Bfk8A", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "id": "AEfvMSxShUQpZSuePNWDE", - "type": "text", - "x": 2214.25, - "y": 3184.5625, - "width": 109.66666412353516, - "height": 70, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [ - "EqchHnvxW4F3CZlxS3ZLf" - ], - "frameId": null, - "roundness": null, - "seed": 1826988625, - "version": 169, - "versionNonce": 1204324511, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "text": "Enqueue\nevent", - "fontSize": 28, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 60, - "containerId": null, - "originalText": "Enqueue\nevent", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "diamond", - "version": 182, - "versionNonce": 1566119121, - "isDeleted": false, - "id": "FzvO9GAfyGZ18nkeg6sqg", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2598, - "y": 3125.8125, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 1408074929, - "groupIds": [ - "sGFmIKliorGun3Z4v_ThM" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "v5XNvHAzT8hEPB-xS8Lm-", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 184, - "versionNonce": 269229247, - "isDeleted": false, - "id": "8ygQDctY0-Jc5NdB5y13U", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2626.716667175293, - "y": 3184.5625, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 114.73332977294922, - "height": 70, - "seed": 169592465, - "groupIds": [ - "sGFmIKliorGun3Z4v_ThM" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Schedule\nevent", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Schedule\nevent", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "diamond", - "version": 215, - "versionNonce": 1541427889, - "isDeleted": false, - "id": "N1kaZPXe4w-0uMCvbk26s", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2808, - "y": 3125.8125, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 1430844337, - "groupIds": [ - "mNI_x2ZtP8vvQgtwe-I5W" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "nJnHUIaYOEbFPI4Um_SSP", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 223, - "versionNonce": 252194015, - "isDeleted": false, - "id": "rWD2W5qhMgSbyeNqh-MN1", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2845.3333320617676, - "y": 3184.5625, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 97.5, - "height": 70, - "seed": 1278301585, - "groupIds": [ - "mNI_x2ZtP8vvQgtwe-I5W" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Remove\nevent", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Remove\nevent", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "diamond", - "version": 187, - "versionNonce": 1347238143, - "isDeleted": false, - "id": "a7ZzGXNievWuIRjc_r6sO", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2388, - "y": 3125.8125, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 220615199, - "groupIds": [ - "chkNCTnbxWDQrMnVAvlI1" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "JgDS5iqVOqEGYS58f5c2w", - "type": "arrow" - }, - { - "id": "X03XGZhIrAVW527YyyaQ9", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 191, - "versionNonce": 285088369, - "isDeleted": false, - "id": "DoIzmswE8BOHSJqJY85e-", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2449.7166652679443, - "y": 3184.5625, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 48.733333587646484, - "height": 70, - "seed": 1240774207, - "groupIds": [ - "chkNCTnbxWDQrMnVAvlI1" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Run\nOne", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Run\nOne", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "id": "TqgUZf3Ae5kEF3T0Bfk8A", - "type": "arrow", - "x": 2269.5577692205798, - "y": 3127.0100198741875, - "width": 2.548632042351528, - "height": 550.2600198741875, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 965557823, - "version": 94, - "versionNonce": 425443615, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 2.548632042351528, - -550.2600198741875 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "DL1gyY_9x_rKNazOi8w4q", - "focus": -0.0009316770186335404, - "gap": 3.503524392907984 - }, - "endBinding": { - "elementId": "gDVZ5MXGm-jMUQGgHS5Bo", - "focus": 0.17969146238377007, - "gap": 11.5625 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "nJnHUIaYOEbFPI4Um_SSP", - "type": "arrow", - "x": 2900.2379456105195, - "y": 3127.1192438135313, - "width": 7.769548704342924, - "height": 547.1997774103693, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1026621535, - "version": 221, - "versionNonce": 202613841, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 7.769548704342924, - -547.1997774103693 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "N1kaZPXe4w-0uMCvbk26s", - "focus": 0.05492738693935681, - "gap": 3.401352695043151 - }, - "endBinding": { - "elementId": "rDDQqXj8Ohi190IEyyQuk", - "focus": -0.6578118987726732, - "gap": 14.731966403162005 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "BU6Q_chjX1v5Ezp1pPYIg", - "type": "text", - "x": 2750.5, - "y": 2645.5, - "width": 149.10000610351562, - "height": 75, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1810032049, - "version": 136, - "versionNonce": 732185919, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "text": "Remove planned\naction from\nevent planner", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "right", - "verticalAlign": "top", - "baseline": 68, - "containerId": null, - "originalText": "Remove planned\naction from\nevent planner", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "lqXmoYc-xnzUo1tfY-hFy", - "type": "text", - "x": 2594.25, - "y": 2388, - "width": 68.5999984741211, - "height": 35, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1212333407, - "version": 29, - "versionNonce": 60617265, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "text": "Clock", - "fontSize": 28, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 25, - "containerId": null, - "originalText": "Clock", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "rectangle", - "version": 187, - "versionNonce": 1183270239, - "isDeleted": false, - "id": "gDVZ5MXGm-jMUQGgHS5Bo", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2101.125, - "y": 2302.6875, - "strokeColor": "#1e1e1e", - "backgroundColor": "#b2f2bb", - "width": 418.75, - "height": 262.5, - "seed": 2021740209, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "5acqEJ3rQmcNR8oYOthsB", - "type": "arrow" - }, - { - "id": "X03XGZhIrAVW527YyyaQ9", - "type": "arrow" - }, - { - "id": "TqgUZf3Ae5kEF3T0Bfk8A", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false - }, - { - "id": "v5XNvHAzT8hEPB-xS8Lm-", - "type": "arrow", - "x": 2681.9469010616017, - "y": 3122.9312706900396, - "width": 14.201861018350883, - "height": 538.6812706900396, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 680723039, - "version": 96, - "versionNonce": 1924717041, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -14.201861018350883, - -538.6812706900396 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "FzvO9GAfyGZ18nkeg6sqg", - "focus": 0.0013060330299966294, - "gap": 3.9375336983131106 - }, - "endBinding": { - "elementId": "rDDQqXj8Ohi190IEyyQuk", - "focus": 0.5043468913922092, - "gap": 19.0625 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "5acqEJ3rQmcNR8oYOthsB", - "type": "arrow", - "x": 2670.5, - "y": 2750.5, - "width": 167.85351125733223, - "height": 182.5, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 2008367793, - "version": 72, - "versionNonce": 1431222687, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -62.5, - -106.25 - ], - [ - -167.85351125733223, - -182.5 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "nT_P_M2skHjB1FMpa6UH1", - "focus": 0.7340194486114147, - "gap": 6.3000030517578125 - }, - "endBinding": { - "elementId": "gDVZ5MXGm-jMUQGgHS5Bo", - "focus": -0.017696225675852165, - "gap": 2.8125 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "nT_P_M2skHjB1FMpa6UH1", - "type": "text", - "x": 2553, - "y": 2708, - "width": 111.19999694824219, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 94339615, - "version": 122, - "versionNonce": 2001368017, - "isDeleted": false, - "boundElements": [ - { - "id": "5acqEJ3rQmcNR8oYOthsB", - "type": "arrow" - } - ], - "updated": 1687761516833, - "link": null, - "locked": false, - "text": "if time is\nin the past", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 43, - "containerId": null, - "originalText": "if time is\nin the past", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "JgDS5iqVOqEGYS58f5c2w", - "type": "arrow", - "x": 2469.525522410118, - "y": 3126.600364459081, - "width": 110.9744775898821, - "height": 553.600364459081, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1741992511, - "version": 58, - "versionNonce": 1336159679, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 32.2244775898821, - -354.850364459081 - ], - [ - 110.9744775898821, - -553.600364459081 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "a7ZzGXNievWuIRjc_r6sO", - "focus": -0.1510099872343621, - "gap": 4.80555230912335 - }, - "endBinding": { - "elementId": "rDDQqXj8Ohi190IEyyQuk", - "focus": 0.5184976314008573, - "gap": 7.8125 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "dWYL3IKQNsTQKYpoBcJS0", - "type": "text", - "x": 2493, - "y": 2939.25, - "width": 155.10000610351562, - "height": 100, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1619870993, - "version": 134, - "versionNonce": 239980977, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "text": "(1) if there are\noverdue actions\ntransfer them\nto event queue", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 93, - "containerId": null, - "originalText": "(1) if there are\noverdue actions\ntransfer them\nto event queue", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "-HIZWXDzY1pAmLlVOD9Xi", - "type": "text", - "x": 2684.25, - "y": 2844.25, - "width": 137.9499969482422, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 247229599, - "version": 35, - "versionNonce": 594943455, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "text": "add action to\nevent planner", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 43, - "containerId": null, - "originalText": "add action to\nevent planner", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "X03XGZhIrAVW527YyyaQ9", - "type": "arrow", - "x": 2469.9253130930274, - "y": 3126.1407987514285, - "width": 35.65670158338253, - "height": 554.3907987514285, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 669698097, - "version": 71, - "versionNonce": 216691601, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516833, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -10.675313093027398, - -348.1407987514285 - ], - [ - -35.65670158338253, - -554.3907987514285 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "a7ZzGXNievWuIRjc_r6sO", - "focus": -0.017483321831147917, - "gap": 4.837002258790385 - }, - "endBinding": { - "elementId": "gDVZ5MXGm-jMUQGgHS5Bo", - "focus": -0.4753204322694144, - "gap": 6.5625 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "type": "text", - "version": 267, - "versionNonce": 1674906111, - "isDeleted": false, - "id": "LJLiuaDr7LZPF-fzRMkYO", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2286.2499923706055, - "y": 2770.5, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 167.6666717529297, - "height": 75, - "seed": 719177009, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516833, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "(2) run & remove\n first action of\nevent queue", - "textAlign": "right", - "verticalAlign": "top", - "containerId": null, - "originalText": "(2) run & remove\n first action of\nevent queue", - "lineHeight": 1.25, - "baseline": 68 - }, - { - "id": "FOVaIImynPEkjBoBYC7Ro", - "type": "text", - "x": 2115.050003051758, - "y": 2718, - "width": 137.9499969482422, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 983111345, - "version": 54, - "versionNonce": 928504177, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516834, - "link": null, - "locked": false, - "text": "add action to\nevent queue", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "right", - "verticalAlign": "top", - "baseline": 43, - "containerId": null, - "originalText": "add action to\nevent queue", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "vI4K05avrBeu3cQMvUPJo", - "type": "text", - "x": 2144.8583335876465, - "y": 2386.6435509453127, - "width": 199.5833282470703, - "height": 105, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1080672113, - "version": 205, - "versionNonce": 1591575377, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516834, - "link": null, - "locked": false, - "text": "* action ae3b\n* action 75f1\n* action 22c2", - "fontSize": 28, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 95, - "containerId": null, - "originalText": "* action ae3b\n* action 75f1\n* action 22c2", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "9_CyDwKkRWFxOWRUFIcYt", - "type": "text", - "x": 2199.050003051758, - "y": 2321.75, - "width": 217.89999389648438, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 156798943, - "version": 30, - "versionNonce": 707515967, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516834, - "link": null, - "locked": false, - "text": "Event queue", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "Event queue", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "rectangle", - "version": 176, - "versionNonce": 267695711, - "isDeleted": false, - "id": "6Tv_ADYniDCATb6iTLPJU", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2478.625, - "y": 3493.46875, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 936.250000000001, - "height": 923.7500000000001, - "seed": 571608017, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "zfEOqY99AwW_7OgaBdk-0", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 161, - "versionNonce": 530555665, - "isDeleted": false, - "id": "IU8JfeANFfznzTTlSh5O9", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2634.6918953789605, - "y": 3519.71875, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 577.9833374023438, - "height": 61.25, - "seed": 744647089, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 49, - "fontFamily": 1, - "text": "Scheduler (thread safe)", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Scheduler (thread safe)", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "rectangle", - "version": 231, - "versionNonce": 1710002815, - "isDeleted": false, - "id": "E5MFRHnW_d9W5B5zP7xXj", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2953.625, - "y": 3613.15625, - "strokeColor": "#1e1e1e", - "backgroundColor": "#a5d8ff", - "width": 418.75, - "height": 262.5, - "seed": 749472657, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "cufEE1onz2oUIvtT6B8Nt", - "type": "arrow" - }, - { - "id": "jmuVDAAyHCfqcPW2cPJz3", - "type": "arrow" - }, - { - "id": "b75fKaE2GrLsHHH7__mIr", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 153, - "versionNonce": 716904689, - "isDeleted": false, - "id": "sZ9rWzY67huKv_IApEa5S", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3052.125, - "y": 3636.59375, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 238, - "height": 45, - "seed": 455504241, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Event planner", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Event planner", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "diamond", - "version": 259, - "versionNonce": 313316049, - "isDeleted": false, - "id": "NG3CQVUnFiL6Fn3jrRLh-", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2574.875, - "y": 4436.281250000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 1121683761, - "groupIds": [ - "VBnp7N-7ovhCxsBRGiNW0" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "zfEOqY99AwW_7OgaBdk-0", - "type": "arrow" - }, - { - "id": "y8mfCk3MSYMd0E_H9NjpV", - "type": "arrow" - }, - { - "id": "9hCEcoZXojYZRp0cNMnz3", - "type": "arrow" - }, - { - "id": "q_llY42cI1tVe3L9-Gxo1", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 250, - "versionNonce": 1499152063, - "isDeleted": false, - "id": "LKnPCtxu2B_nkEUHtUS_8", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2606.125, - "y": 4495.031250000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 109.66666412353516, - "height": 70, - "seed": 1708473105, - "groupIds": [ - "VBnp7N-7ovhCxsBRGiNW0" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Enqueue\nevent", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Enqueue\nevent", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "diamond", - "version": 263, - "versionNonce": 1812746417, - "isDeleted": false, - "id": "8jqh5CgRIq5_R2IGSDQ4n", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2989.875, - "y": 4436.281250000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 1182642417, - "groupIds": [ - "b9pwQ77HEeVMxA_v-d4K3" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "jmuVDAAyHCfqcPW2cPJz3", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 265, - "versionNonce": 1713573599, - "isDeleted": false, - "id": "8xNXjgMKaSROP-SrII0PS", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3018.591667175293, - "y": 4495.031250000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 114.73332977294922, - "height": 70, - "seed": 1247570641, - "groupIds": [ - "b9pwQ77HEeVMxA_v-d4K3" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Schedule\nevent", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Schedule\nevent", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "diamond", - "version": 296, - "versionNonce": 1283527313, - "isDeleted": false, - "id": "KYvjx5SpYnkfkQbQBDIqP", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3199.875, - "y": 4436.281250000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 973662385, - "groupIds": [ - "3FxUoou4ckP1JZGF3Cct8" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "cufEE1onz2oUIvtT6B8Nt", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 304, - "versionNonce": 96197375, - "isDeleted": false, - "id": "majRMjfXqA6bKGIK2hveq", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3237.2083320617676, - "y": 4495.031250000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 97.5, - "height": 70, - "seed": 1596631697, - "groupIds": [ - "3FxUoou4ckP1JZGF3Cct8" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Remove\nevent", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Remove\nevent", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "diamond", - "version": 269, - "versionNonce": 683841649, - "isDeleted": false, - "id": "9jJMdnQoVJMeteIGXNMs1", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2779.875, - "y": 4436.281250000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 1652109425, - "groupIds": [ - "sN85hdTm4w09ORyB1LUry" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "b75fKaE2GrLsHHH7__mIr", - "type": "arrow" - }, - { - "id": "Mu89sidR-iUQQqWLPZivP", - "type": "arrow" - }, - { - "id": "uTDs7vlzLBVSNFxo4_Cy4", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 272, - "versionNonce": 749085471, - "isDeleted": false, - "id": "gCCAAP6GnLw8h-iDBj1Y0", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2841.5916652679443, - "y": 4495.031250000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 48.733333587646484, - "height": 70, - "seed": 1773926993, - "groupIds": [ - "sN85hdTm4w09ORyB1LUry" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Run\nOne", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Run\nOne", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "arrow", - "version": 339, - "versionNonce": 1922589265, - "isDeleted": false, - "id": "zfEOqY99AwW_7OgaBdk-0", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2661.432769220579, - "y": 4437.478769874188, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 2.548632042351528, - "height": 550.2600198741875, - "seed": 1976207409, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "startBinding": { - "elementId": "6Tv_ADYniDCATb6iTLPJU", - "focus": -0.601968204443141, - "gap": 20.2600198741884 - }, - "endBinding": { - "elementId": "x-CgWWGHK2EZx0OHr3Ilr", - "focus": 0.17969146238377645, - "gap": 11.56250000000091 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 2.548632042351528, - -550.2600198741875 - ] - ] - }, - { - "type": "arrow", - "version": 466, - "versionNonce": 1800562495, - "isDeleted": false, - "id": "cufEE1onz2oUIvtT6B8Nt", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3292.112945610519, - "y": 4437.587993813532, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 7.769548704342924, - "height": 547.1997774103693, - "seed": 9198097, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "startBinding": { - "elementId": "KYvjx5SpYnkfkQbQBDIqP", - "focus": 0.05492738693935154, - "gap": 3.4013526950428243 - }, - "endBinding": { - "elementId": "E5MFRHnW_d9W5B5zP7xXj", - "focus": -0.657811898772671, - "gap": 14.731966403162915 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 7.769548704342924, - -547.1997774103693 - ] - ] - }, - { - "type": "text", - "version": 217, - "versionNonce": 1993994289, - "isDeleted": false, - "id": "DK1tiOoBIshNHUkRwI_lH", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3142.375, - "y": 3955.96875, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 149.10000610351562, - "height": 75, - "seed": 1526263793, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "Remove planned\naction from\nevent planner", - "textAlign": "right", - "verticalAlign": "top", - "containerId": null, - "originalText": "Remove planned\naction from\nevent planner", - "lineHeight": 1.25, - "baseline": 68 - }, - { - "type": "text", - "version": 110, - "versionNonce": 1881220959, - "isDeleted": false, - "id": "mK-MZ-cN4dWnaPsOdAice", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2986.125, - "y": 3698.46875, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 68.5999984741211, - "height": 35, - "seed": 1259850193, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Clock", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Clock", - "lineHeight": 1.25, - "baseline": 25 - }, - { - "type": "rectangle", - "version": 270, - "versionNonce": 2070856209, - "isDeleted": false, - "id": "x-CgWWGHK2EZx0OHr3Ilr", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2493, - "y": 3613.15625, - "strokeColor": "#1e1e1e", - "backgroundColor": "#b2f2bb", - "width": 418.75, - "height": 262.5, - "seed": 1778411441, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "RFRb_7P7GIBFCIKczxuAr", - "type": "arrow" - }, - { - "id": "Mu89sidR-iUQQqWLPZivP", - "type": "arrow" - }, - { - "id": "zfEOqY99AwW_7OgaBdk-0", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false - }, - { - "type": "arrow", - "version": 341, - "versionNonce": 1853895121, - "isDeleted": false, - "id": "jmuVDAAyHCfqcPW2cPJz3", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3073.8219010616012, - "y": 4433.400020690041, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 14.201861018350883, - "height": 538.6812706900396, - "seed": 1850726737, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "startBinding": { - "elementId": "8jqh5CgRIq5_R2IGSDQ4n", - "focus": 0.0014974601351553317, - "gap": 3.658020599852371 - }, - "endBinding": { - "elementId": "E5MFRHnW_d9W5B5zP7xXj", - "focus": 0.5043468913922141, - "gap": 19.062500000001364 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -14.201861018350883, - -538.6812706900396 - ] - ] - }, - { - "type": "arrow", - "version": 317, - "versionNonce": 1580289983, - "isDeleted": false, - "id": "RFRb_7P7GIBFCIKczxuAr", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3062.375, - "y": 4060.9687499999995, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 167.85351125733223, - "height": 182.5, - "seed": 445286193, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "startBinding": { - "elementId": "Jz5GigjypEhW25fp1d3cM", - "focus": 0.7340194486114147, - "gap": 6.3000030517578125 - }, - "endBinding": { - "elementId": "x-CgWWGHK2EZx0OHr3Ilr", - "focus": -0.017696225675853546, - "gap": 2.8124999999995453 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -62.5, - -106.25 - ], - [ - -167.85351125733223, - -182.5 - ] - ] - }, - { - "type": "text", - "version": 203, - "versionNonce": 1411108785, - "isDeleted": false, - "id": "Jz5GigjypEhW25fp1d3cM", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2944.875, - "y": 4018.4687499999995, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 111.19999694824219, - "height": 50, - "seed": 720664849, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [ - { - "id": "RFRb_7P7GIBFCIKczxuAr", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "if time is\nin the past", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "if time is\nin the past", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 303, - "versionNonce": 1745813471, - "isDeleted": false, - "id": "b75fKaE2GrLsHHH7__mIr", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2861.4005224101174, - "y": 4437.069114459083, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 110.9744775898821, - "height": 553.600364459081, - "seed": 1627729649, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "startBinding": { - "elementId": "9jJMdnQoVJMeteIGXNMs1", - "focus": -0.14804874416538774, - "gap": 2.8525104347998322 - }, - "endBinding": { - "elementId": "E5MFRHnW_d9W5B5zP7xXj", - "focus": 0.5184976314008563, - "gap": 7.812500000001819 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 32.2244775898821, - -354.850364459081 - ], - [ - 110.9744775898821, - -553.600364459081 - ] - ] - }, - { - "type": "text", - "version": 215, - "versionNonce": 608743825, - "isDeleted": false, - "id": "SsPhNNhM0mHm75aLdZwLf", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2884.875, - "y": 4249.718750000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 155.10000610351562, - "height": 100, - "seed": 1665411281, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "(1) if there are\noverdue actions\ntransfer them\nto event queue", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "(1) if there are\noverdue actions\ntransfer them\nto event queue", - "lineHeight": 1.25, - "baseline": 93 - }, - { - "type": "text", - "version": 116, - "versionNonce": 654420991, - "isDeleted": false, - "id": "TDSOoR6rLYU83bgOAq7Op", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3076.125, - "y": 4154.718750000001, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 137.9499969482422, - "height": 50, - "seed": 457762481, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "add action to\nevent planner", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "add action to\nevent planner", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 316, - "versionNonce": 71194481, - "isDeleted": false, - "id": "Mu89sidR-iUQQqWLPZivP", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2861.8003130930265, - "y": 4436.609548751429, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 35.65670158338253, - "height": 554.3907987514285, - "seed": 1404758161, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "startBinding": { - "elementId": "9jJMdnQoVJMeteIGXNMs1", - "focus": -0.01848322772236595, - "gap": 2.883960384467734 - }, - "endBinding": { - "elementId": "x-CgWWGHK2EZx0OHr3Ilr", - "focus": -0.475320432269413, - "gap": 6.5625000000009095 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -10.675313093027398, - -348.1407987514285 - ], - [ - -35.65670158338253, - -554.3907987514285 - ] - ] - }, - { - "type": "text", - "version": 348, - "versionNonce": 654955551, - "isDeleted": false, - "id": "iSxkbC2kyWAN5UXnlDqKB", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2678.1249923706055, - "y": 4080.9687499999995, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 167.6666717529297, - "height": 75, - "seed": 374451825, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "(2) run & remove\n first action of\nevent queue", - "textAlign": "right", - "verticalAlign": "top", - "containerId": null, - "originalText": "(2) run & remove\n first action of\nevent queue", - "lineHeight": 1.25, - "baseline": 68 - }, - { - "type": "text", - "version": 135, - "versionNonce": 105136465, - "isDeleted": false, - "id": "kRGmNicafeWnaVHk4H5GQ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2506.925003051758, - "y": 4028.4687499999995, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 137.9499969482422, - "height": 50, - "seed": 1238677585, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "add action to\nevent queue", - "textAlign": "right", - "verticalAlign": "top", - "containerId": null, - "originalText": "add action to\nevent queue", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 111, - "versionNonce": 129101617, - "isDeleted": false, - "id": "WG6hoVnST2PfDFT3kpIoa", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2590.925003051758, - "y": 3632.21875, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 217.89999389648438, - "height": 45, - "seed": 1678612497, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516834, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Event queue", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Event queue", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "id": "Jm57IezHFjYGTdi3H_obO", - "type": "ellipse", - "x": 1983.1785714285718, - "y": 4850.857142857143, - "width": 356, - "height": 183, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1694482783, - "version": 118, - "versionNonce": 2014091359, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "t-FRZF7C3VjEDf8KoN4b_" - }, - { - "id": "y8mfCk3MSYMd0E_H9NjpV", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false - }, - { - "id": "t-FRZF7C3VjEDf8KoN4b_", - "type": "text", - "x": 2044.6635628514873, - "y": 4897.156872378574, - "width": 233.3000030517578, - "height": 90, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 89857009, - "version": 164, - "versionNonce": 1324278033, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516834, - "link": null, - "locked": false, - "text": "User request\nFIND_PEER", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "middle", - "baseline": 77, - "containerId": "Jm57IezHFjYGTdi3H_obO", - "originalText": "User request\nFIND_PEER", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "y8mfCk3MSYMd0E_H9NjpV", - "type": "arrow", - "x": 2340.3214285714284, - "y": 4910.857142857143, - "width": 304.2857142857147, - "height": 288.57142857142935, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 504426367, - "version": 41, - "versionNonce": 1769117425, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516834, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 182.85714285714312, - -122.85714285714312 - ], - [ - 304.2857142857147, - -288.57142857142935 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "Jm57IezHFjYGTdi3H_obO", - "focus": 0.5901213528360404, - "gap": 10.095645486448404 - }, - "endBinding": { - "elementId": "NG3CQVUnFiL6Fn3jrRLh-", - "focus": -0.6294285892767792, - "gap": 16.92344255832939 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "4NdFwb2YqDe15AybPitUT", - "type": "text", - "x": 2378.5716925726983, - "y": 3395.1428571428573, - "width": 1133.566650390625, - "height": 73.57142857142837, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 190470655, - "version": 121, - "versionNonce": 1435525169, - "isDeleted": false, - "boundElements": null, - "updated": 1687762594011, - "link": null, - "locked": false, - "text": "Use in a multi threaded app (e.g Kubo)", - "fontSize": 58.8571428571427, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 52, - "containerId": null, - "originalText": "Use in a multi threaded app (e.g Kubo)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "tFBeeHp0V5nrrWu-ZtuhY", - "type": "ellipse", - "x": 3091.7499999999995, - "y": 4806.5714285714275, - "width": 472, - "height": 205, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 671231903, - "version": 300, - "versionNonce": 1237618897, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "B7pfOpPbpazZ_-oeS1VNp" - }, - { - "id": "uTDs7vlzLBVSNFxo4_Cy4", - "type": "arrow" - }, - { - "id": "W-L2xPNUbo-FDEw7lk0_3", - "type": "arrow" - } - ], - "updated": 1687761516834, - "link": null, - "locked": false - }, - { - "id": "B7pfOpPbpazZ_-oeS1VNp", - "type": "text", - "x": 3174.5061309388025, - "y": 4841.592983499806, - "width": 306.73333740234375, - "height": 135, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1659017169, - "version": 376, - "versionNonce": 254173375, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516834, - "link": null, - "locked": false, - "text": "worker picks up \nquery setup\n(concurrency = 2)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "middle", - "baseline": 122, - "containerId": "tFBeeHp0V5nrrWu-ZtuhY", - "originalText": "worker picks up query setup\n(concurrency = 2)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "uTDs7vlzLBVSNFxo4_Cy4", - "type": "arrow", - "x": 2863.1785714285716, - "y": 4618, - "width": 286.70644655600836, - "height": 222.48325484534325, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 401088159, - "version": 175, - "versionNonce": 976875743, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516835, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 78.57142857142844, - 104.28571428571377 - ], - [ - 286.70644655600836, - 222.48325484534325 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "9jJMdnQoVJMeteIGXNMs1", - "focus": 0.8408172381519543, - "gap": 4.181720093227177 - }, - "endBinding": { - "elementId": "tFBeeHp0V5nrrWu-ZtuhY", - "gap": 1, - "focus": -0.1878470806438625 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "aZM5JWrJ7u0MzTcfwDNM9", - "type": "text", - "x": 2444.753572191511, - "y": 4762.285714285714, - "width": 36.849998474121094, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 508324383, - "version": 9, - "versionNonce": 323047569, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516835, - "link": null, - "locked": false, - "text": "(1)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(1)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "xB--1UtLxDHO0CLGg_iRz", - "type": "text", - "x": 2989.669047491891, - "y": 4713.714285714285, - "width": 52.733333587646484, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 719203089, - "version": 34, - "versionNonce": 950435071, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516836, - "link": null, - "locked": false, - "text": "(2)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(2)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "1gGHAkBZspcYyN4fjs6rK", - "type": "ellipse", - "x": 2830.3214285714284, - "y": 4983.714285714285, - "width": 285.7142857142853, - "height": 205, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1198048575, - "version": 40, - "versionNonce": 1758140017, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "6f6hwGBpcWlzj3rNxifLt" - }, - { - "id": "W-L2xPNUbo-FDEw7lk0_3", - "type": "arrow" - }, - { - "id": "9hCEcoZXojYZRp0cNMnz3", - "type": "arrow" - }, - { - "id": "q_llY42cI1tVe3L9-Gxo1", - "type": "arrow" - } - ], - "updated": 1687761516836, - "link": null, - "locked": false - }, - { - "id": "6f6hwGBpcWlzj3rNxifLt", - "type": "text", - "x": 2886.154985674522, - "y": 5018.735840642664, - "width": 174.01666259765625, - "height": 135, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 396385023, - "version": 44, - "versionNonce": 770475295, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516836, - "link": null, - "locked": false, - "text": "worker \nenqueues \n2 queries", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "middle", - "baseline": 122, - "containerId": "1gGHAkBZspcYyN4fjs6rK", - "originalText": "worker enqueues 2 queries", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "W-L2xPNUbo-FDEw7lk0_3", - "type": "arrow", - "x": 3176.342765825444, - "y": 4988.894401674919, - "width": 86.03821668731325, - "height": 37.2268580358068, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 2048421169, - "version": 184, - "versionNonce": 2038204735, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516836, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -86.03821668731325, - 37.2268580358068 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "tFBeeHp0V5nrrWu-ZtuhY", - "gap": 1, - "focus": -0.07026528302522007 - }, - "endBinding": { - "elementId": "1gGHAkBZspcYyN4fjs6rK", - "gap": 1, - "focus": -0.07865975944074961 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "9hCEcoZXojYZRp0cNMnz3", - "type": "arrow", - "x": 2913.1785714285716, - "y": 4976.5714285714275, - "width": 215.71428571428578, - "height": 371.42857142857065, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 380317137, - "version": 47, - "versionNonce": 1364913713, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516836, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -215.71428571428578, - -371.42857142857065 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "1gGHAkBZspcYyN4fjs6rK", - "focus": 0.023760057612596884, - "gap": 15.846477175905164 - }, - "endBinding": { - "elementId": "NG3CQVUnFiL6Fn3jrRLh-", - "focus": 0.11390249243509382, - "gap": 19.282188783357633 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "q_llY42cI1tVe3L9-Gxo1", - "type": "arrow", - "x": 2898.8928571428573, - "y": 4986.5714285714275, - "width": 225.71428571428578, - "height": 368.57142857142753, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 2049415807, - "version": 26, - "versionNonce": 951519583, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516836, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -225.71428571428578, - -368.57142857142753 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "1gGHAkBZspcYyN4fjs6rK", - "focus": -0.0850047512807336, - "gap": 11.147360743452609 - }, - "endBinding": { - "elementId": "NG3CQVUnFiL6Fn3jrRLh-", - "focus": 0.515919158361013, - "gap": 10.734991033847514 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "0hbs0YhbKKpmO1aCQOckh", - "type": "text", - "x": 3134.513096128191, - "y": 5015.142857142857, - "width": 51.61666488647461, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 871412735, - "version": 18, - "versionNonce": 421527569, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516836, - "link": null, - "locked": false, - "text": "(3)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(3)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "X7_Zk3qqy_O6QAn6mG4IR", - "type": "text", - "x": 2719.54047530038, - "y": 4793.714285714285, - "width": 50.13333511352539, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 537756927, - "version": 21, - "versionNonce": 149718399, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516836, - "link": null, - "locked": false, - "text": "(4)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(4)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "Lrbpv8oMndNeub50jmbMC", - "type": "text", - "x": 2805.646429334368, - "y": 4752.285714285715, - "width": 49.349998474121094, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 75261905, - "version": 28, - "versionNonce": 774362609, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516836, - "link": null, - "locked": false, - "text": "(5)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(5)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "rectangle", - "version": 214, - "versionNonce": 1130291103, - "isDeleted": false, - "id": "zNSG5TsmKlIQ0X90CMTIW", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2494.5535714285716, - "y": 5550.979910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 936.250000000001, - "height": 923.7500000000001, - "seed": 1335415007, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "Ql7mTILoeQd8fAilpU_-D", - "type": "arrow" - } - ], - "updated": 1687761516837, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 199, - "versionNonce": 1312271825, - "isDeleted": false, - "id": "R63_NI7rblfCxBioNf8j6", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2650.620466807532, - "y": 5577.229910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 577.9833374023438, - "height": 61.25, - "seed": 2133417215, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516837, - "link": null, - "locked": false, - "fontSize": 49, - "fontFamily": 1, - "text": "Scheduler (thread safe)", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Scheduler (thread safe)", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "rectangle", - "version": 271, - "versionNonce": 664832351, - "isDeleted": false, - "id": "F7LU9e69ZXU3jDpYDpbPg", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2969.5535714285716, - "y": 5670.667410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#a5d8ff", - "width": 418.75, - "height": 262.5, - "seed": 1628661023, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "zcwEV_R0iEB5RKrsxL3wa", - "type": "arrow" - }, - { - "id": "DbrZ9UpLzL5n_57JmsEAX", - "type": "arrow" - }, - { - "id": "HMpSHWyDcu1i0sihKnCDF", - "type": "arrow" - } - ], - "updated": 1687762433963, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 191, - "versionNonce": 1199819697, - "isDeleted": false, - "id": "SoDeNV0hQe-sxgadXKfm9", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3068.0535714285716, - "y": 5694.104910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 238, - "height": 45, - "seed": 1690790207, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516837, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Event planner", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Event planner", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "diamond", - "version": 302, - "versionNonce": 1632710033, - "isDeleted": false, - "id": "bi6gtuzkwd1zxy5hEFfy6", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2590.8035714285716, - "y": 6493.792410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 117400959, - "groupIds": [ - "aliCjN1uIl6c0ELxyX_sO" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "Ql7mTILoeQd8fAilpU_-D", - "type": "arrow" - }, - { - "id": "D6rKMzS9DPQS8fX3EHk4u", - "type": "arrow" - }, - { - "id": "jskVcG5mo62CSIX4pN84k", - "type": "arrow" - } - ], - "updated": 1687761516837, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 289, - "versionNonce": 2043604991, - "isDeleted": false, - "id": "rQEc_Yf-Rs-5tJQOAXdXn", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2622.0535714285716, - "y": 6552.542410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 109.66666412353516, - "height": 70, - "seed": 34889119, - "groupIds": [ - "aliCjN1uIl6c0ELxyX_sO" - ], - "frameId": null, - "roundness": null, - "boundElements": [ - { - "id": "D6rKMzS9DPQS8fX3EHk4u", - "type": "arrow" - } - ], - "updated": 1687761516837, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Enqueue\nevent", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Enqueue\nevent", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "diamond", - "version": 303, - "versionNonce": 47666033, - "isDeleted": false, - "id": "sT4terWiYxPCcxhIyVLME", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3005.8035714285716, - "y": 6493.792410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 471159231, - "groupIds": [ - "CkdqU2RyaCY1dSJw0W-K1" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "DbrZ9UpLzL5n_57JmsEAX", - "type": "arrow" - }, - { - "id": "7h6wemmNDBdZx82ceGVSw", - "type": "arrow" - }, - { - "id": "6cvOlWovLx9tuTaZtZ8Gi", - "type": "arrow" - } - ], - "updated": 1687761516837, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 303, - "versionNonce": 932091935, - "isDeleted": false, - "id": "1SGi0PFwdhJjqjtVTmkph", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3034.5202386038645, - "y": 6552.542410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 114.73332977294922, - "height": 70, - "seed": 237158879, - "groupIds": [ - "CkdqU2RyaCY1dSJw0W-K1" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516837, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Schedule\nevent", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Schedule\nevent", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "diamond", - "version": 335, - "versionNonce": 1079885137, - "isDeleted": false, - "id": "m8K6Be8FWx5kRrRwGmQIP", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3215.8035714285716, - "y": 6493.792410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 424488447, - "groupIds": [ - "M7blzJcq0FelvrRt_DzkX" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "zcwEV_R0iEB5RKrsxL3wa", - "type": "arrow" - }, - { - "id": "l4OE3dq7E-hvh8xgVGfQm", - "type": "arrow" - } - ], - "updated": 1687761516837, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 342, - "versionNonce": 1264860223, - "isDeleted": false, - "id": "L2KnWED9TC_dT_8M8ZiHa", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3253.136903490339, - "y": 6552.542410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 97.5, - "height": 70, - "seed": 143988255, - "groupIds": [ - "M7blzJcq0FelvrRt_DzkX" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516837, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Remove\nevent", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Remove\nevent", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "diamond", - "version": 319, - "versionNonce": 206902161, - "isDeleted": false, - "id": "2BtTstoyLCGsiY2S7HLuY", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2795.8035714285716, - "y": 6493.792410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 172.5, - "height": 178.75, - "seed": 1928002111, - "groupIds": [ - "JU3EIz5kkUaCTdY23wXES" - ], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "HMpSHWyDcu1i0sihKnCDF", - "type": "arrow" - }, - { - "id": "3OLybxyV8hbqA8bDD8syn", - "type": "arrow" - }, - { - "id": "jJIWoTLJS_Q5RJGEvO-YS", - "type": "arrow" - }, - { - "id": "0buH7HpxnKK52GoGa6Nc_", - "type": "arrow" - }, - { - "id": "G1xcT5JIEubMwfZombxse", - "type": "arrow" - }, - { - "id": "ijajb7Ynw7Ml7TDC-fxoc", - "type": "arrow" - }, - { - "id": "sYzsV-FDrYb9P9wb19_97", - "type": "arrow" - }, - { - "id": "hRgPhDd2QSKt5dg4jmNs1", - "type": "arrow" - }, - { - "id": "x8aM4kFjBWtdo1BNX1DDU", - "type": "arrow" - }, - { - "id": "gM07ZRTN9g49j8P3nxqmj", - "type": "arrow" - } - ], - "updated": 1687762161826, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 310, - "versionNonce": 2033212511, - "isDeleted": false, - "id": "kMHxpZqna00-_MUpDX0Na", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2857.520236696516, - "y": 6552.542410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 48.733333587646484, - "height": 70, - "seed": 1026513503, - "groupIds": [ - "JU3EIz5kkUaCTdY23wXES" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516837, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Run\nOne", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Run\nOne", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "type": "arrow", - "version": 451, - "versionNonce": 903437585, - "isDeleted": false, - "id": "Ql7mTILoeQd8fAilpU_-D", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2677.3613406491504, - "y": 6494.989930588475, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 2.548632042351528, - "height": 550.2600198741875, - "seed": 1546827391, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516837, - "link": null, - "locked": false, - "startBinding": { - "elementId": "zNSG5TsmKlIQ0X90CMTIW", - "focus": -0.601968204443141, - "gap": 20.26001987418931 - }, - "endBinding": { - "elementId": "2NTbtP15O0t0X3wUZEAz2", - "focus": 0.17969146238377645, - "gap": 11.562500000001819 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 2.548632042351528, - -550.2600198741875 - ] - ] - }, - { - "type": "arrow", - "version": 582, - "versionNonce": 1587938687, - "isDeleted": false, - "id": "zcwEV_R0iEB5RKrsxL3wa", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3308.217144152095, - "y": 6495.281144942019, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 7.611431969208752, - "height": 547.3817678245696, - "seed": 1454739103, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687762433963, - "link": null, - "locked": false, - "startBinding": { - "elementId": "m8K6Be8FWx5kRrRwGmQIP", - "focus": 0.05492738693935185, - "gap": 3.4013526950415596 - }, - "endBinding": { - "elementId": "F7LU9e69ZXU3jDpYDpbPg", - "focus": -0.6578118987726712, - "gap": 14.731966403163824 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 7.611431969208752, - -547.3817678245696 - ] - ] - }, - { - "type": "text", - "version": 255, - "versionNonce": 95261425, - "isDeleted": false, - "id": "ah5OaFzSlYK9AflAv8cLN", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3158.3035714285716, - "y": 6013.479910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 149.10000610351562, - "height": 75, - "seed": 124281535, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516837, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "Remove planned\naction from\nevent planner", - "textAlign": "right", - "verticalAlign": "top", - "containerId": null, - "originalText": "Remove planned\naction from\nevent planner", - "lineHeight": 1.25, - "baseline": 68 - }, - { - "type": "text", - "version": 157, - "versionNonce": 876216383, - "isDeleted": false, - "id": "CT7GErJnOILAUaIWGsLW9", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2993.482142857143, - "y": 5738.837053571428, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 68.5999984741211, - "height": 35, - "seed": 518763231, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687762479003, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "Clock", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Clock", - "lineHeight": 1.25, - "baseline": 25 - }, - { - "type": "rectangle", - "version": 306, - "versionNonce": 490736849, - "isDeleted": false, - "id": "2NTbtP15O0t0X3wUZEAz2", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2508.9285714285716, - "y": 5670.667410714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#b2f2bb", - "width": 418.75, - "height": 262.5, - "seed": 1708130047, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 3 - }, - "boundElements": [ - { - "id": "YgnOm7hBnkHk_ZuziMeC_", - "type": "arrow" - }, - { - "id": "3OLybxyV8hbqA8bDD8syn", - "type": "arrow" - }, - { - "id": "Ql7mTILoeQd8fAilpU_-D", - "type": "arrow" - } - ], - "updated": 1687761516837, - "link": null, - "locked": false - }, - { - "type": "arrow", - "version": 457, - "versionNonce": 1810300319, - "isDeleted": false, - "id": "DbrZ9UpLzL5n_57JmsEAX", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3089.9287949172935, - "y": 6490.814754003706, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 14.355751548534045, - "height": 538.5848432894181, - "seed": 2105002847, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687762433963, - "link": null, - "locked": false, - "startBinding": { - "elementId": "sT4terWiYxPCcxhIyVLME", - "focus": 0.0014974601351547994, - "gap": 3.658020599851106 - }, - "endBinding": { - "elementId": "F7LU9e69ZXU3jDpYDpbPg", - "focus": 0.5043468913922142, - "gap": 19.06250000000273 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -14.355751548534045, - -538.5848432894181 - ] - ] - }, - { - "type": "arrow", - "version": 429, - "versionNonce": 588106897, - "isDeleted": false, - "id": "YgnOm7hBnkHk_ZuziMeC_", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3078.3035714285716, - "y": 6118.479910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 167.85351125733223, - "height": 182.5, - "seed": 1011156863, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "startBinding": { - "elementId": "j3V7HgDM0puot8fD3bHbW", - "focus": 0.7340194486114147, - "gap": 6.3000030517578125 - }, - "endBinding": { - "elementId": "2NTbtP15O0t0X3wUZEAz2", - "focus": -0.01769622567585184, - "gap": 2.8125 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -62.5, - -106.25 - ], - [ - -167.85351125733223, - -182.5 - ] - ] - }, - { - "type": "text", - "version": 241, - "versionNonce": 921682175, - "isDeleted": false, - "id": "j3V7HgDM0puot8fD3bHbW", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2960.8035714285716, - "y": 6075.979910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 111.19999694824219, - "height": 50, - "seed": 39174047, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [ - { - "id": "YgnOm7hBnkHk_ZuziMeC_", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "if time is\nin the past", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "if time is\nin the past", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 417, - "versionNonce": 1749577681, - "isDeleted": false, - "id": "HMpSHWyDcu1i0sihKnCDF", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2877.329093838689, - "y": 6494.580275173367, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 110.9744775898821, - "height": 553.600364459081, - "seed": 299529151, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687762433963, - "link": null, - "locked": false, - "startBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": -0.14804874416538774, - "gap": 2.8525104347998322 - }, - "endBinding": { - "elementId": "F7LU9e69ZXU3jDpYDpbPg", - "focus": 0.5184976314008577, - "gap": 7.8125000000009095 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 32.2244775898821, - -354.850364459081 - ], - [ - 110.9744775898821, - -553.600364459081 - ] - ] - }, - { - "type": "text", - "version": 253, - "versionNonce": 443369759, - "isDeleted": false, - "id": "_PBPxm_ewSwqefjGwvqoh", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2900.8035714285716, - "y": 6307.229910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 155.10000610351562, - "height": 100, - "seed": 317957087, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "(1) if there are\noverdue actions\ntransfer them\nto event queue", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "(1) if there are\noverdue actions\ntransfer them\nto event queue", - "lineHeight": 1.25, - "baseline": 93 - }, - { - "type": "text", - "version": 154, - "versionNonce": 79600721, - "isDeleted": false, - "id": "WCyO9V-jQzDVAZ3MmlBST", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3092.0535714285716, - "y": 6212.229910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 137.9499969482422, - "height": 50, - "seed": 1437716479, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "add action to\nevent planner", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "add action to\nevent planner", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "arrow", - "version": 428, - "versionNonce": 558157119, - "isDeleted": false, - "id": "3OLybxyV8hbqA8bDD8syn", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2877.728884521598, - "y": 6494.120709465714, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 35.65670158338253, - "height": 554.3907987514285, - "seed": 1410693151, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "startBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": -0.01848322772236595, - "gap": 2.883960384467734 - }, - "endBinding": { - "elementId": "2NTbtP15O0t0X3wUZEAz2", - "focus": -0.4753204322694134, - "gap": 6.5625 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - -10.675313093027398, - -348.1407987514285 - ], - [ - -35.65670158338253, - -554.3907987514285 - ] - ] - }, - { - "type": "text", - "version": 386, - "versionNonce": 472443441, - "isDeleted": false, - "id": "DWKVMgc0iM9ayHj_BlF-C", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2694.053563799177, - "y": 6138.479910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 167.6666717529297, - "height": 75, - "seed": 1947147327, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "(2) run & remove\n first action of\nevent queue", - "textAlign": "right", - "verticalAlign": "top", - "containerId": null, - "originalText": "(2) run & remove\n first action of\nevent queue", - "lineHeight": 1.25, - "baseline": 68 - }, - { - "type": "text", - "version": 173, - "versionNonce": 1188482399, - "isDeleted": false, - "id": "OT3g4F_IVbK3eo5uGnoek", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2522.8535744803294, - "y": 6085.979910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ff8787", - "width": 137.9499969482422, - "height": 50, - "seed": 159333471, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "add action to\nevent queue", - "textAlign": "right", - "verticalAlign": "top", - "containerId": null, - "originalText": "add action to\nevent queue", - "lineHeight": 1.25, - "baseline": 43 - }, - { - "type": "text", - "version": 366, - "versionNonce": 837078175, - "isDeleted": false, - "id": "KhNOjxgEu9WZExjnOWBnl", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2552.661905016218, - "y": 5754.623461659598, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 333.0333251953125, - "height": 175, - "seed": 406214783, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687762415158, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "* action ae3b (query 0)\n* action 75f1 (query 1)\n* action d47d (query 2)\n* action 1e5a (query 3)\n", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "* action ae3b (query 0)\n* action 75f1 (query 1)\n* action d47d (query 2)\n* action 1e5a (query 3)\n", - "lineHeight": 1.25, - "baseline": 165 - }, - { - "type": "text", - "version": 149, - "versionNonce": 1181993343, - "isDeleted": false, - "id": "S7nfeerV7V3Y7KJKv46sJ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2606.8535744803294, - "y": 5689.729910714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 217.89999389648438, - "height": 45, - "seed": 1442115743, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "Event queue", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Event queue", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "text", - "version": 159, - "versionNonce": 1380562783, - "isDeleted": false, - "id": "XqrbnQzFyZODS37gSmfv0", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2394.50026400127, - "y": 5452.654017857142, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 1133.566650390625, - "height": 73.57142857142837, - "seed": 1225764159, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687762594015, - "link": null, - "locked": false, - "fontSize": 58.8571428571427, - "fontFamily": 1, - "text": "Use in a multi threaded app (e.g Kubo)", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "Use in a multi threaded app (e.g Kubo)", - "lineHeight": 1.25, - "baseline": 52 - }, - { - "type": "text", - "version": 379, - "versionNonce": 698600145, - "isDeleted": false, - "id": "NNPBgUVLKVrmF5T0rRUg4", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2512.384525844029, - "y": 3694.0714285714294, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 381.8999938964844, - "height": 70, - "seed": 1766594271, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 28, - "fontFamily": 1, - "text": "* action 178e (query setup)\n", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "* action 178e (query setup)\n", - "lineHeight": 1.25, - "baseline": 60 - }, - { - "id": "dqU92izi4E8uTMat3f_Ix", - "type": "ellipse", - "x": 2366.035714285714, - "y": 6909.85714285714, - "width": 317, - "height": 141, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1818133759, - "version": 153, - "versionNonce": 191874751, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "jT-MC2I6sjFQBBegiyhHr" - }, - { - "id": "jJIWoTLJS_Q5RJGEvO-YS", - "type": "arrow" - }, - { - "id": "ZcBgFKouRZxYsb2CbQqoQ", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "id": "jT-MC2I6sjFQBBegiyhHr", - "type": "text", - "x": 2420.034286415889, - "y": 6935.506114783489, - "width": 208.85000610351562, - "height": 90, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 785648753, - "version": 273, - "versionNonce": 2036615345, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "text": "worker picks\nup query 0", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "middle", - "baseline": 77, - "containerId": "dqU92izi4E8uTMat3f_Ix", - "originalText": "worker picks up query 0", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "x9ynLXgy1eEFRd-n3i6Gn", - "type": "ellipse", - "x": 2596.035714285714, - "y": 4775.142857142857, - "width": 1215.7142857142858, - "height": 448.57142857142753, - "angle": 0, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 386522015, - "version": 36, - "versionNonce": 1952880351, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "ellipse", - "version": 116, - "versionNonce": 1545210513, - "isDeleted": false, - "id": "0ByGdNTbsaqmv4o-xPWB4", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 5.719141848641485, - "x": 1949.777487054081, - "y": 4636.792793494214, - "strokeColor": "#2f9e44", - "backgroundColor": "transparent", - "width": 652.8833431540527, - "height": 448.57142857142753, - "seed": 2135043121, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "id": "YENhUCIHuOreR_evWpZ5I", - "type": "text", - "x": 1998.077386038644, - "y": 4625.142857142857, - "width": 295.9166564941406, - "height": 45, - "angle": 5.719141848641485, - "strokeColor": "#2f9e44", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 424437137, - "version": 34, - "versionNonce": 307127039, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "text": "Caller go routine", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "Caller go routine", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "ellipse", - "version": 217, - "versionNonce": 1600686655, - "isDeleted": false, - "id": "LhBYl9Ck9WBc0wwqjSC77", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2145.3214285714294, - "y": 6642.285714285708, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "width": 1215.7142857142858, - "height": 2472.857142857142, - "seed": 2000070207, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "id": "05mMaAGbop9meInlHhF5U", - "type": "arrow" - } - ], - "updated": 1687762268299, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 150, - "versionNonce": 363309855, - "isDeleted": false, - "id": "rahvw3lpCkwl1kouUvzMe", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0.08800737352774757, - "x": 3171.8345227922714, - "y": 4714.071428571428, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "width": 414.1166687011719, - "height": 45, - "seed": 2071469215, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "single worker go routine", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "single worker go routine", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 205, - "versionNonce": 978541137, - "isDeleted": false, - "id": "--_7WQzKdJju4suWtS8uG", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2387.535714285715, - "y": 7328.928571428569, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 317, - "height": 141, - "seed": 291970751, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "TkBisi8BWbNAEVX_i7cu8" - }, - { - "id": "Q7JCmPDOAW02FwzONLbTg", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 328, - "versionNonce": 1256386367, - "isDeleted": false, - "id": "TkBisi8BWbNAEVX_i7cu8", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2441.5342864158897, - "y": 7354.577543354917, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 208.85000610351562, - "height": 90, - "seed": 1907551967, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "worker picks\nup query 1", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "--_7WQzKdJju4suWtS8uG", - "originalText": "worker picks up query 1", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 488, - "versionNonce": 153994865, - "isDeleted": false, - "id": "yTRHZWW47ZRASFsCCGN4M", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2773.75, - "y": 7301.785714285713, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 445, - "height": 205, - "seed": 1051354065, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "FKPKRUus26xIKup-2-OzP" - }, - { - "id": "Q7JCmPDOAW02FwzONLbTg", - "type": "arrow" - }, - { - "id": "wU2W9UTGJRzvYn1lrI5gt", - "type": "arrow" - }, - { - "id": "x8aM4kFjBWtdo1BNX1DDU", - "type": "arrow" - } - ], - "updated": 1687762098515, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 780, - "versionNonce": 1984712689, - "isDeleted": false, - "id": "FKPKRUus26xIKup-2-OzP", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2846.1854037836492, - "y": 7336.807269214091, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 300.4666748046875, - "height": 135, - "seed": 1143980465, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "worker selects \nclosest peer\n(not queried yet)", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "yTRHZWW47ZRASFsCCGN4M", - "originalText": "worker selects closest peer\n(not queried yet)", - "lineHeight": 1.25, - "baseline": 122 - }, - { - "type": "ellipse", - "version": 468, - "versionNonce": 1987396191, - "isDeleted": false, - "id": "ChAZ14sZHvqijmnOHV4-2", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2724.9642857142867, - "y": 6889.785714285713, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 445, - "height": 205, - "seed": 1460891199, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "OSUvkTp-u6fI_egm0XTyH" - }, - { - "id": "ZcBgFKouRZxYsb2CbQqoQ", - "type": "arrow" - }, - { - "id": "_G5BBEFip6J5CBGhdkPYi", - "type": "arrow" - }, - { - "id": "hRgPhDd2QSKt5dg4jmNs1", - "type": "arrow" - } - ], - "updated": 1687762078181, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 760, - "versionNonce": 1694443985, - "isDeleted": false, - "id": "OSUvkTp-u6fI_egm0XTyH", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2797.399689497936, - "y": 6924.807269214091, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 300.4666748046875, - "height": 135, - "seed": 1814646367, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "worker selects \nclosest peer\n(not queried yet)", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "ChAZ14sZHvqijmnOHV4-2", - "originalText": "worker selects closest peer\n(not queried yet)", - "lineHeight": 1.25, - "baseline": 122 - }, - { - "type": "ellipse", - "version": 578, - "versionNonce": 268158897, - "isDeleted": false, - "id": "kAdi0WiuBgYgQky764l6d", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3312.107142857143, - "y": 6919.78571428571, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 290, - "height": 141, - "seed": 1188235039, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "6bRRj9SXPDdxCpO2AEeWB" - }, - { - "id": "RzLwg0_obgXvNNNJ3priR", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 923, - "versionNonce": 1414170591, - "isDeleted": false, - "id": "6bRRj9SXPDdxCpO2AEeWB", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3362.709990883922, - "y": 6945.434686212058, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 188.73333740234375, - "height": 90, - "seed": 1984088895, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "query peer\nover libp2p", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "kAdi0WiuBgYgQky764l6d", - "originalText": "query peer over libp2p", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 641, - "versionNonce": 1365879185, - "isDeleted": false, - "id": "eNP6d5uk5w9E4WiF6tvK3", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3673.5357142857138, - "y": 6921.214285714285, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 365, - "height": 141, - "seed": 1688844561, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "3XKPO0_jwn3vhsgFl2GEh" - }, - { - "id": "RzLwg0_obgXvNNNJ3priR", - "type": "arrow" - }, - { - "id": "7h6wemmNDBdZx82ceGVSw", - "type": "arrow" - }, - { - "id": "THiThVWGywziTqtY8EFyW", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 976, - "versionNonce": 311178239, - "isDeleted": false, - "id": "3XKPO0_jwn3vhsgFl2GEh", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3733.580393894462, - "y": 6946.8632576406335, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 244.81666564941406, - "height": 90, - "seed": 840068849, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "schedule \ntimeout event", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "eNP6d5uk5w9E4WiF6tvK3", - "originalText": "schedule timeout event", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 847, - "versionNonce": 1342397471, - "isDeleted": false, - "id": "epyouFviExs0s4Ml5psQS", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4130.678571428571, - "y": 6867.499999999999, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 259, - "height": 269, - "seed": 1628464593, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "KcX1-R1y1BFVZUfGPAt66" - }, - { - "id": "THiThVWGywziTqtY8EFyW", - "type": "arrow" - }, - { - "id": "qDlvndYTPz7GwL0SMkYam", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1368, - "versionNonce": 696727889, - "isDeleted": false, - "id": "KcX1-R1y1BFVZUfGPAt66", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4176.858243264913, - "y": 6911.894137930409, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 166.5, - "height": 180, - "seed": 1854243761, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "receives \nresponse \nbefore \ntimeout", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "epyouFviExs0s4Ml5psQS", - "originalText": "receives response before timeout", - "lineHeight": 1.25, - "baseline": 167 - }, - { - "type": "ellipse", - "version": 899, - "versionNonce": 10518591, - "isDeleted": false, - "id": "t7OUhKjpTq1D1v4u1kz2P", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4766.75, - "y": 6850.3571428571395, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 235, - "height": 269, - "seed": 1333279487, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "zzsEq_o9PUly2CSUmNGZu" - }, - { - "id": "qDlvndYTPz7GwL0SMkYam", - "type": "arrow" - }, - { - "id": "d7g0rupoqQKIib7kqrJAF", - "type": "arrow" - }, - { - "id": "D6rKMzS9DPQS8fX3EHk4u", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1435, - "versionNonce": 936765233, - "isDeleted": false, - "id": "zzsEq_o9PUly2CSUmNGZu", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4806.931623437631, - "y": 6894.751280787549, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 154.46665954589844, - "height": 180, - "seed": 1071022879, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "enqueue \nhandle \nresponse\naction", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "t7OUhKjpTq1D1v4u1kz2P", - "originalText": "enqueue handle response action", - "lineHeight": 1.25, - "baseline": 167 - }, - { - "type": "ellipse", - "version": 160, - "versionNonce": 1178869855, - "isDeleted": false, - "id": "xa1BNeZKVSC93jgdxv13Y", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3266.7500000000014, - "y": 6815.142857142856, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 1900.000000000001, - "height": 341.42857142856974, - "seed": 549464447, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "id": "o-SRA-TNM5YNwb5Bt6M4D", - "type": "text", - "x": 3898.2571367536275, - "y": 6752.285714285712, - "width": 452.70001220703125, - "height": 45, - "angle": 0, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 407379487, - "version": 82, - "versionNonce": 1205066001, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "text": "libp2p message go routine", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "libp2p message go routine", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "ellipse", - "version": 627, - "versionNonce": 851360895, - "isDeleted": false, - "id": "u8cFgITKeJmr1UEwCME3u", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3341.3928571428564, - "y": 7356.214285714283, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 290, - "height": 141, - "seed": 832324319, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "kt7lAjIC4Y_ugjdAdq1Xp" - }, - { - "id": "wU2W9UTGJRzvYn1lrI5gt", - "type": "arrow" - }, - { - "id": "l3OsQAd9LhEOOdgSfsEVR", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 971, - "versionNonce": 1794674417, - "isDeleted": false, - "id": "kt7lAjIC4Y_ugjdAdq1Xp", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3391.995705169635, - "y": 7381.863257640631, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 188.73333740234375, - "height": 90, - "seed": 1945014015, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "query peer\nover libp2p", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "u8cFgITKeJmr1UEwCME3u", - "originalText": "query peer over libp2p", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 688, - "versionNonce": 1238422687, - "isDeleted": false, - "id": "IFxbvzDWpsyRvc39yqEu8", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3702.8214285714266, - "y": 7357.642857142859, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 365, - "height": 141, - "seed": 711087903, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "epPALkrxO926iotC30DQO" - }, - { - "id": "0VTQkdSFyTfZ1cv9eFGoy", - "type": "arrow" - }, - { - "id": "6cvOlWovLx9tuTaZtZ8Gi", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1024, - "versionNonce": 399214801, - "isDeleted": false, - "id": "epPALkrxO926iotC30DQO", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3762.8661081801756, - "y": 7383.291829069207, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 244.81666564941406, - "height": 90, - "seed": 1265113919, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "schedule \ntimeout event", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "IFxbvzDWpsyRvc39yqEu8", - "originalText": "schedule timeout event", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 781, - "versionNonce": 1126885567, - "isDeleted": false, - "id": "nA_AGTUGp1zprrKWCUqDZ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4124.249999999996, - "y": 7321.0714285714275, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 450, - "height": 205, - "seed": 727735135, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "rOCGsaSgc-xvOlEFTo4Z1" - }, - { - "id": "0VTQkdSFyTfZ1cv9eFGoy", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1292, - "versionNonce": 132400817, - "isDeleted": false, - "id": "rOCGsaSgc-xvOlEFTo4Z1", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4196.442645985953, - "y": 7356.092983499806, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 305.4166564941406, - "height": 135, - "seed": 1050384255, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "receives response\nafter timeout or\nno response", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "nA_AGTUGp1zprrKWCUqDZ", - "originalText": "receives response after timeout or no response", - "lineHeight": 1.25, - "baseline": 122 - }, - { - "type": "ellipse", - "version": 228, - "versionNonce": 187237631, - "isDeleted": false, - "id": "3_NhHe6Cps6xAOclWDMET", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3296.035714285715, - "y": 7251.5714285714275, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 1318.5714285714298, - "height": 341.42857142856974, - "seed": 1323999, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 164, - "versionNonce": 1536495217, - "isDeleted": false, - "id": "wcIRu7SHbB-7-7nO6OFoL", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3861.8285653250546, - "y": 7187.285714285714, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 452.70001220703125, - "height": 45, - "seed": 1883267071, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "libp2p message go routine", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "libp2p message go routine", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "id": "jJIWoTLJS_Q5RJGEvO-YS", - "type": "arrow", - "x": 2873.1785714285716, - "y": 6670.85714285714, - "width": 234.28571428571422, - "height": 250, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 251997105, - "version": 80, - "versionNonce": 1660752191, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -94.28571428571422, - 155.71428571428532 - ], - [ - -234.28571428571422, - 250 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": -0.5127134499591485, - "gap": 5.215951688026983 - }, - "endBinding": { - "elementId": "dqU92izi4E8uTMat3f_Ix", - "focus": 0.13692459703517895, - "gap": 9.752658541960045 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "ZcBgFKouRZxYsb2CbQqoQ", - "type": "arrow", - "x": 2684.607142857143, - "y": 6990.85714285714, - "width": 38.57142857142844, - "height": 0, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 521279711, - "version": 20, - "versionNonce": 604431921, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 38.57142857142844, - 0 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "dqU92izi4E8uTMat3f_Ix", - "focus": 0.14893617021276587, - "gap": 3.17765983571374 - }, - "endBinding": { - "elementId": "ChAZ14sZHvqijmnOHV4-2", - "focus": 0.013937282229975303, - "gap": 1.8065291210913585 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "_G5BBEFip6J5CBGhdkPYi", - "type": "arrow", - "x": 3170.3214285714284, - "y": 6989.428571428569, - "width": 145.71428571428578, - "height": 5.714285714286234, - "angle": 0, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 388246897, - "version": 18, - "versionNonce": 210525297, - "isDeleted": false, - "boundElements": null, - "updated": 1687761978218, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 145.71428571428578, - 5.714285714286234 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "ChAZ14sZHvqijmnOHV4-2", - "focus": -0.11273022220001086, - "gap": 1 - }, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "RzLwg0_obgXvNNNJ3priR", - "type": "arrow", - "x": 3603.1785714285716, - "y": 7000.85714285714, - "width": 71.4285714285711, - "height": 1.4285714285715585, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 288614033, - "version": 18, - "versionNonce": 596441105, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 71.4285714285711, - -1.4285714285715585 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "kAdi0WiuBgYgQky764l6d", - "focus": 0.19122632900184278, - "gap": 2.59590971644667 - }, - "endBinding": { - "elementId": "eNP6d5uk5w9E4WiF6tvK3", - "focus": -0.0578758793704587, - "gap": 1 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "7h6wemmNDBdZx82ceGVSw", - "type": "arrow", - "x": 3746.035714285714, - "y": 6930.85714285714, - "width": 578.5714285714284, - "height": 315.7142857142853, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1852446719, - "version": 55, - "versionNonce": 1642794367, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -578.5714285714284, - -315.7142857142853 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "eNP6d5uk5w9E4WiF6tvK3", - "focus": 0.006822523848179481, - "gap": 4.42126277274221 - }, - "endBinding": { - "elementId": "sT4terWiYxPCcxhIyVLME", - "focus": -0.10265284098619237, - "gap": 14.404548696747021 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "THiThVWGywziTqtY8EFyW", - "type": "arrow", - "x": 4039.39712970539, - "y": 6988.935990138341, - "width": 91.04698961158874, - "height": 1.5913794892676378, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 191202879, - "version": 197, - "versionNonce": 1345703409, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 91.04698961158874, - -1.5913794892676378 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "eNP6d5uk5w9E4WiF6tvK3", - "gap": 1, - "focus": -0.004149716926504326 - }, - "endBinding": { - "elementId": "epyouFviExs0s4Ml5psQS", - "gap": 1, - "focus": 0.1258035539632223 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "sROmooJ7YDBp6Z1b7Ou7r", - "type": "text", - "x": 4038.9607135227743, - "y": 6927.999999999997, - "width": 74.1500015258789, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 850681695, - "version": 25, - "versionNonce": 1363871135, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "text": "wait", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "wait", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "qDlvndYTPz7GwL0SMkYam", - "type": "arrow", - "x": 4396.3632666838075, - "y": 6999.171076023392, - "width": 61.484848042032354, - "height": 4.2296687709122125, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1891775793, - "version": 424, - "versionNonce": 17360849, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 61.484848042032354, - -4.2296687709122125 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "epyouFviExs0s4Ml5psQS", - "focus": 0.04851452014749695, - "gap": 6.712029054565278 - }, - "endBinding": { - "elementId": "sQqMPzpRcZ_b52BdO83gS", - "focus": -0.019773989713620457, - "gap": 1 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "type": "ellipse", - "version": 914, - "versionNonce": 506919359, - "isDeleted": false, - "id": "sQqMPzpRcZ_b52BdO83gS", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4458.178571428573, - "y": 6881.785714285712, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 250, - "height": 205, - "seed": 1563397439, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "A6tba1Pp4BK4gDTOueHdB" - }, - { - "id": "qDlvndYTPz7GwL0SMkYam", - "type": "arrow" - }, - { - "id": "d7g0rupoqQKIib7kqrJAF", - "type": "arrow" - }, - { - "id": "l4OE3dq7E-hvh8xgVGfQm", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1479, - "versionNonce": 1075692977, - "isDeleted": false, - "id": "A6tba1Pp4BK4gDTOueHdB", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4500.448556604962, - "y": 6916.80726921409, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 165.68333435058594, - "height": 135, - "seed": 929664351, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "remove \nscheduled\ntimeout", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "sQqMPzpRcZ_b52BdO83gS", - "originalText": "remove scheduled timeout", - "lineHeight": 1.25, - "baseline": 122 - }, - { - "id": "d7g0rupoqQKIib7kqrJAF", - "type": "arrow", - "x": 4710.321428571428, - "y": 6989.428571428569, - "width": 52.85714285714312, - "height": 0, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1915698897, - "version": 15, - "versionNonce": 768567775, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 52.85714285714312, - 0 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "sQqMPzpRcZ_b52BdO83gS", - "focus": 0.05017421602787204, - "gap": 2.2961065289517677 - }, - "endBinding": { - "elementId": "t7OUhKjpTq1D1v4u1kz2P", - "focus": -0.033988316516203355, - "gap": 3.637748513906928 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "l4OE3dq7E-hvh8xgVGfQm", - "type": "arrow", - "x": 4550.321428571428, - "y": 6876.571428571426, - "width": 1165.7142857142858, - "height": 275.7142857142853, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1126756255, - "version": 66, - "versionNonce": 1755047825, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -267.1428571428578, - -170 - ], - [ - -1165.7142857142858, - -275.7142857142853 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "sQqMPzpRcZ_b52BdO83gS", - "focus": 0.6690453336558116, - "gap": 8.618415014546073 - }, - "endBinding": { - "elementId": "m8K6Be8FWx5kRrRwGmQIP", - "focus": 0.08925927014160058, - "gap": 9.624156347356852 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "D6rKMzS9DPQS8fX3EHk4u", - "type": "arrow", - "x": 4918.892857142857, - "y": 6830.85714285714, - "width": 2182.8571428571427, - "height": 205.71428571428532, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1498360479, - "version": 196, - "versionNonce": 1807873873, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -574.2857142857138, - -175.71428571428532 - ], - [ - -1402.8571428571427, - -90 - ], - [ - -1895.7142857142853, - -97.14285714285779 - ], - [ - -2182.8571428571427, - -205.71428571428532 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "t7OUhKjpTq1D1v4u1kz2P", - "focus": 1.1822822785861695, - "gap": 24.300159644606055 - }, - "endBinding": { - "elementId": "rQEc_Yf-Rs-5tJQOAXdXn", - "focus": 0.2733698459944884, - "gap": 4.315478733607506 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "GIKWko3q3WvQCVMfJC5wT", - "type": "arrow", - "x": 2856.035714285714, - "y": 6663.7142857142835, - "width": 512.8571428571427, - "height": 670, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1223754417, - "version": 61, - "versionNonce": 537453873, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -512.8571428571427, - 278.57142857142844 - ], - [ - -425.7142857142858, - 670 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "d6n4g_or0vnbNmIIEEA-f", - "type": "text", - "x": 2691.8964293343683, - "y": 6825.142857142855, - "width": 36.849998474121094, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1198140529, - "version": 20, - "versionNonce": 1833435743, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "text": "(1)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(1)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "qoN5VtJB9cXT95x-WBa8z", - "type": "text", - "x": 2375.3833332061768, - "y": 7157.999999999997, - "width": 52.733333587646484, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1878717713, - "version": 10, - "versionNonce": 1828132625, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "text": "(2)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(2)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "Q7JCmPDOAW02FwzONLbTg", - "type": "arrow", - "x": 2708.8928571428573, - "y": 7399.428571428569, - "width": 60, - "height": 1.4285714285715585, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1723285905, - "version": 13, - "versionNonce": 1391877759, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 60, - -1.4285714285715585 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "--_7WQzKdJju4suWtS8uG", - "focus": 0.05492209373692461, - "gap": 4.3571428571422075 - }, - "endBinding": { - "elementId": "yTRHZWW47ZRASFsCCGN4M", - "focus": 0.1139842478688366, - "gap": 5.234159330963308 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "wU2W9UTGJRzvYn1lrI5gt", - "type": "arrow", - "x": 3224.6071428571427, - "y": 7402.285714285712, - "width": 110, - "height": 11.428571428570649, - "angle": 0, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1089265329, - "version": 20, - "versionNonce": 2114106417, - "isDeleted": false, - "boundElements": null, - "updated": 1687761984014, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 110, - 11.428571428570649 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "yTRHZWW47ZRASFsCCGN4M", - "focus": -0.2448303765329087, - "gap": 5.894801781492845 - }, - "endBinding": { - "elementId": "u8cFgITKeJmr1UEwCME3u", - "focus": -0.03842255428583639, - "gap": 8.76648750982244 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "l3OsQAd9LhEOOdgSfsEVR", - "type": "arrow", - "x": 3634.6071428571427, - "y": 7429.428571428569, - "width": 68.5714285714289, - "height": 1.4285714285715585, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1850523135, - "version": 10, - "versionNonce": 113522335, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 68.5714285714289, - -1.4285714285715585 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "u8cFgITKeJmr1UEwCME3u", - "focus": 0.08222360726145364, - "gap": 3.3123208653755967 - }, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "0VTQkdSFyTfZ1cv9eFGoy", - "type": "arrow", - "x": 4070.3214285714284, - "y": 7419.428571428569, - "width": 52.85714285714312, - "height": 4.285714285713766, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1241194143, - "version": 15, - "versionNonce": 1605846737, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 52.85714285714312, - 4.285714285713766 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "IFxbvzDWpsyRvc39yqEu8", - "focus": -0.3291996952891886, - "gap": 3.732244651782736 - }, - "endBinding": { - "elementId": "nA_AGTUGp1zprrKWCUqDZ", - "focus": -0.17743564353667288, - "gap": 1.0716421976079573 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "type": "text", - "version": 41, - "versionNonce": 258083519, - "isDeleted": false, - "id": "RwbIKnNtZfiHmsJAKyVIs", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4053.246427808489, - "y": 7345.499999999996, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 74.1500015258789, - "height": 45, - "seed": 1648647089, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761516838, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "wait", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "wait", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 370, - "versionNonce": 415032351, - "isDeleted": false, - "id": "Zcp6tmXeXkg3N6F0I-TAD", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2216.107142857143, - "y": 7787.214285714283, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 373, - "height": 141, - "seed": 1257843793, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "2hGotHmUXpMiQO2sfrgHv" - }, - { - "id": "aG3k1BYcBP8f_RIsNEhqv", - "type": "arrow" - }, - { - "id": "0buH7HpxnKK52GoGa6Nc_", - "type": "arrow" - } - ], - "updated": 1687761544920, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 528, - "versionNonce": 151662929, - "isDeleted": false, - "id": "2hGotHmUXpMiQO2sfrgHv", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2278.998398392903, - "y": 7812.863257640631, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 247.46665954589844, - "height": 90, - "seed": 1553037873, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761544920, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "worker picks \nup response 0", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "Zcp6tmXeXkg3N6F0I-TAD", - "originalText": "worker picks up response 0", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "id": "6cvOlWovLx9tuTaZtZ8Gi", - "type": "arrow", - "x": 3740.3214285714284, - "y": 7385.142857142855, - "width": 614.2857142857138, - "height": 731.4285714285716, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1800736977, - "version": 107, - "versionNonce": 142509713, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -411.4285714285711, - -300.0000000000009 - ], - [ - -614.2857142857138, - -731.4285714285716 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "IFxbvzDWpsyRvc39yqEu8", - "focus": -0.41654402365283044, - "gap": 1 - }, - "endBinding": { - "elementId": "sT4terWiYxPCcxhIyVLME", - "focus": -0.00940400778798232, - "gap": 11.378164968787665 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "j_o05oJs46tL55n3SIOkH", - "type": "ellipse", - "x": 2664.6071428571436, - "y": 7726.571428571424, - "width": 322, - "height": 269, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 267260255, - "version": 232, - "versionNonce": 696231679, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "kl4lNZrtWZ3QxvBGfEurx" - }, - { - "id": "aG3k1BYcBP8f_RIsNEhqv", - "type": "arrow" - }, - { - "id": "ZzkwrQxn5U7rb_lNUnJRS", - "type": "arrow" - } - ], - "updated": 1687761516838, - "link": null, - "locked": false - }, - { - "id": "kl4lNZrtWZ3QxvBGfEurx", - "type": "text", - "x": 2718.7129480343515, - "y": 7770.965566501834, - "width": 214.10000610351562, - "height": 180, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 647970335, - "version": 326, - "versionNonce": 1827769457, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "text": "update \nrouting \ntable and \nquery state", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "middle", - "baseline": 167, - "containerId": "j_o05oJs46tL55n3SIOkH", - "originalText": "update routing table and query state", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "ellipse", - "version": 419, - "versionNonce": 1317826993, - "isDeleted": false, - "id": "mVa4UcAqU0qGbffqABfdN", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3025.25, - "y": 7788.928571428567, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 258, - "height": 141, - "seed": 1455240479, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "QNlVEBZuszv4lgh38Eu43" - }, - { - "id": "ZzkwrQxn5U7rb_lNUnJRS", - "type": "arrow" - }, - { - "id": "jskVcG5mo62CSIX4pN84k", - "type": "arrow" - }, - { - "id": "gM07ZRTN9g49j8P3nxqmj", - "type": "arrow" - } - ], - "updated": 1687762161825, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 597, - "versionNonce": 1119293521, - "isDeleted": false, - "id": "QNlVEBZuszv4lgh38Eu43", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3076.799895453986, - "y": 7814.577543354915, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 154.46665954589844, - "height": 90, - "seed": 615987519, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761563553, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "enqueue \nquery 2", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "mVa4UcAqU0qGbffqABfdN", - "originalText": "enqueue query 2", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "id": "aG3k1BYcBP8f_RIsNEhqv", - "type": "arrow", - "x": 2597.7232322320606, - "y": 7861.721361875492, - "width": 59.726222106084606, - "height": 3.0746342110669502, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 402146687, - "version": 65, - "versionNonce": 473554737, - "isDeleted": false, - "boundElements": null, - "updated": 1687761544920, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 59.726222106084606, - -3.0746342110669502 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "Zcp6tmXeXkg3N6F0I-TAD", - "focus": 0.21502280073445534, - "gap": 8.865255344168702 - }, - "endBinding": { - "elementId": "j_o05oJs46tL55n3SIOkH", - "focus": 0.08223254767197836, - "gap": 7.182318676034612 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "ZzkwrQxn5U7rb_lNUnJRS", - "type": "arrow", - "x": 2988.8928571428573, - "y": 7857.999999999996, - "width": 34.285714285714675, - "height": 4.2857142857146755, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1118989983, - "version": 55, - "versionNonce": 182182961, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 34.285714285714675, - 4.2857142857146755 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "j_o05oJs46tL55n3SIOkH", - "focus": -0.17266624297615296, - "gap": 2.3268467986684698 - }, - "endBinding": { - "elementId": "mVa4UcAqU0qGbffqABfdN", - "focus": -0.2660525213514448, - "gap": 2.1717539067360576 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "jskVcG5mo62CSIX4pN84k", - "type": "arrow", - "x": 3205.1026273009693, - "y": 7783.318660775812, - "width": 537.1428571428573, - "height": 1123.8900893472428, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 447577617, - "version": 189, - "versionNonce": 405097233, - "isDeleted": false, - "boundElements": null, - "updated": 1687762146809, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 38.07594412760227, - -598.1758036329566 - ], - [ - -104.78119872954085, - -906.747232204385 - ], - [ - -499.06691301525507, - -1123.8900893472428 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "mVa4UcAqU0qGbffqABfdN", - "focus": 0.3564353245973784, - "gap": 11.035956371001745 - }, - "endBinding": { - "elementId": "bi6gtuzkwd1zxy5hEFfy6", - "focus": 0.6746850975111603, - "gap": 11.748375804867614 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "0buH7HpxnKK52GoGa6Nc_", - "type": "arrow", - "x": 2846.035714285714, - "y": 6645.142857142855, - "width": 544.2857142857133, - "height": 1131.6225302475132, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1478845055, - "version": 97, - "versionNonce": 1144760415, - "isDeleted": false, - "boundElements": null, - "updated": 1687761544920, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -544.2857142857133, - 312.8571428571422 - ], - [ - -498.5583518184044, - 1131.6225302475132 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": -0.4617876218269718, - "gap": 6.890886150294769 - }, - "endBinding": { - "elementId": "Zcp6tmXeXkg3N6F0I-TAD", - "focus": -0.27130068299191973, - "gap": 13.704320692437065 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "KkdnCqcGv_9aYV4vIDJ3Y", - "type": "text", - "x": 2345.941667556763, - "y": 7657.999999999996, - "width": 51.61666488647461, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1287912369, - "version": 25, - "versionNonce": 1994436479, - "isDeleted": false, - "boundElements": null, - "updated": 1687761516838, - "link": null, - "locked": false, - "text": "(3)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(3)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "Atp5IigwlIPzwbO6j6dKj", - "type": "arrow", - "x": 1948.8928571428573, - "y": 6512.285714285712, - "width": 7.14285714285711, - "height": 2314.2857142857138, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 380636209, - "version": 107, - "versionNonce": 243002993, - "isDeleted": false, - "boundElements": null, - "updated": 1687761882699, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -7.14285714285711, - 2314.2857142857138 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "u5wmvpVKJUggM1sryMG0a", - "type": "text", - "x": 1612.109393165225, - "y": 7111.037853495778, - "width": 245.14999389648438, - "height": 139.1050036470759, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 528877617, - "version": 87, - "versionNonce": 433533457, - "isDeleted": false, - "boundElements": null, - "updated": 1687762594016, - "link": null, - "locked": false, - "text": "Time", - "fontSize": 111.28400291766073, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 98, - "containerId": null, - "originalText": "Time", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "RlMd6H9_lT-X8rJqqP6UQ", - "type": "line", - "x": 1916.0357142857144, - "y": 7729.428571428569, - "width": 77.14285714285711, - "height": 1.4285714285715585, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 354956639, - "version": 58, - "versionNonce": 1604772433, - "isDeleted": false, - "boundElements": null, - "updated": 1687762350091, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 77.14285714285711, - -1.4285714285715585 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": null - }, - { - "id": "s-lyfLi8VBM0130xxYmy_", - "type": "text", - "x": 1514.7488054547994, - "y": 7705.142857142854, - "width": 359.7166748046875, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1837351857, - "version": 76, - "versionNonce": 2087446335, - "isDeleted": false, - "boundElements": null, - "updated": 1687762350091, - "link": null, - "locked": false, - "text": "response to query 0", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "response to query 0", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "ellipse", - "version": 222, - "versionNonce": 1882627057, - "isDeleted": false, - "id": "ND1xIwz8NVyMmM6Y8NSnm", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2258.5357142857133, - "y": 8148.714483259268, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 317, - "height": 141, - "seed": 192205055, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "LIUOkx9RqJW3hiMQHWRwC" - }, - { - "id": "B7VNdNs-llWc3PRyhHydC", - "type": "arrow" - }, - { - "id": "G1xcT5JIEubMwfZombxse", - "type": "arrow" - } - ], - "updated": 1687761629495, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 343, - "versionNonce": 90456625, - "isDeleted": false, - "id": "LIUOkx9RqJW3hiMQHWRwC", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2312.534286415888, - "y": 8174.363455185616, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 208.85000610351562, - "height": 90, - "seed": 1003510047, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761609204, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "worker picks\nup query 2", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "ND1xIwz8NVyMmM6Y8NSnm", - "originalText": "worker picks up query 2", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 535, - "versionNonce": 2114604497, - "isDeleted": false, - "id": "ToXQc6u63lfDm-D69YBL7", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2617.464285714286, - "y": 8128.64305468784, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 445, - "height": 205, - "seed": 1137040703, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "N3KQ71AJhqQu39_GwAsCd" - }, - { - "id": "B7VNdNs-llWc3PRyhHydC", - "type": "arrow" - }, - { - "id": "8g-SasWxTFTmExr5Lvtdq", - "type": "arrow" - }, - { - "id": "IN-cN5thwLGEuuOPzckqg", - "type": "arrow" - } - ], - "updated": 1687762200147, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 827, - "versionNonce": 2016735615, - "isDeleted": false, - "id": "N3KQ71AJhqQu39_GwAsCd", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2689.899689497935, - "y": 8163.6646096162185, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 300.4666748046875, - "height": 135, - "seed": 381017439, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761609204, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "worker selects \nclosest peer\n(not queried yet)", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "ToXQc6u63lfDm-D69YBL7", - "originalText": "worker selects closest peer\n(not queried yet)", - "lineHeight": 1.25, - "baseline": 122 - }, - { - "type": "ellipse", - "version": 663, - "versionNonce": 1217660767, - "isDeleted": false, - "id": "u8EFoC648bJAWgHoFXslU", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3330.321428571427, - "y": 8147.214483259268, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 290, - "height": 141, - "seed": 1743837567, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "4UAspGK85imfd7hPENOGs" - }, - { - "id": "xo8KLh4h1KQPyMzDLUE81", - "type": "arrow" - }, - { - "id": "8g-SasWxTFTmExr5Lvtdq", - "type": "arrow" - } - ], - "updated": 1687761676223, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1007, - "versionNonce": 1386910783, - "isDeleted": false, - "id": "4UAspGK85imfd7hPENOGs", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3380.924276598206, - "y": 8172.863455185616, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 188.73333740234375, - "height": 90, - "seed": 750211487, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761670500, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "query peer\nover libp2p", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "u8EFoC648bJAWgHoFXslU", - "originalText": "query peer over libp2p", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 726, - "versionNonce": 1619449919, - "isDeleted": false, - "id": "6Qv6TKFjYt30YdgNZ--xo", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3691.7499999999977, - "y": 8148.643054687842, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 365, - "height": 141, - "seed": 1580844479, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "_37HIextHsXBrcT5veOZ9" - }, - { - "id": "xo8KLh4h1KQPyMzDLUE81", - "type": "arrow" - }, - { - "id": "4WmrbB0O7PJx2miJ_VLWw", - "type": "arrow" - }, - { - "id": "KSRSCJu8mbbshHxyt3ooP", - "type": "arrow" - } - ], - "updated": 1687761692994, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1060, - "versionNonce": 1998554385, - "isDeleted": false, - "id": "_37HIextHsXBrcT5veOZ9", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3751.794679608746, - "y": 8174.29202661419, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 244.81666564941406, - "height": 90, - "seed": 2073605599, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761670500, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "schedule \ntimeout event", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "6Qv6TKFjYt30YdgNZ--xo", - "originalText": "schedule timeout event", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 931, - "versionNonce": 200194801, - "isDeleted": false, - "id": "GT8b38cLKWR03P4bbdv8D", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4148.892857142854, - "y": 8094.928768973556, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 259, - "height": 269, - "seed": 1273274879, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "7y1PDKbG5OlR7VBfmAqu0" - }, - { - "id": "4WmrbB0O7PJx2miJ_VLWw", - "type": "arrow" - }, - { - "id": "OfHxFKz9aNsAZISEq1rT2", - "type": "arrow" - } - ], - "updated": 1687761670500, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1453, - "versionNonce": 2074306719, - "isDeleted": false, - "id": "7y1PDKbG5OlR7VBfmAqu0", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4195.072528979196, - "y": 8139.322906903964, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 166.5, - "height": 180, - "seed": 1612581407, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761670500, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "receives \nresponse \nbefore \ntimeout", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "GT8b38cLKWR03P4bbdv8D", - "originalText": "receives response before timeout", - "lineHeight": 1.25, - "baseline": 167 - }, - { - "type": "ellipse", - "version": 984, - "versionNonce": 1501138591, - "isDeleted": false, - "id": "cWRoM9NnvU2lEFYMK2sBY", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4784.964285714287, - "y": 8077.785911830697, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 235, - "height": 269, - "seed": 1429564991, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "UxXltZOrlvivhmmYWcsy4" - }, - { - "id": "OfHxFKz9aNsAZISEq1rT2", - "type": "arrow" - }, - { - "id": "nF_gkqMcktV2xD9QCN5Lh", - "type": "arrow" - }, - { - "id": "1gwjmSrfL7LWgA1nJ4Y9j", - "type": "arrow" - } - ], - "updated": 1687761706008, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1519, - "versionNonce": 1907035359, - "isDeleted": false, - "id": "UxXltZOrlvivhmmYWcsy4", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4825.145909151918, - "y": 8122.180049761106, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 154.46665954589844, - "height": 180, - "seed": 1443581535, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761670500, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "enqueue \nhandle \nresponse\naction", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "cWRoM9NnvU2lEFYMK2sBY", - "originalText": "enqueue handle response action", - "lineHeight": 1.25, - "baseline": 167 - }, - { - "type": "ellipse", - "version": 244, - "versionNonce": 462690559, - "isDeleted": false, - "id": "Oi7hPcsI6UUaehF6Cbu09", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3284.964285714286, - "y": 8042.571626116414, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 1900.000000000001, - "height": 341.42857142856974, - "seed": 1567631999, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761670500, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 166, - "versionNonce": 706034289, - "isDeleted": false, - "id": "TD4Tp8a3MBkPC53z7d25H", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3916.4714224679115, - "y": 7979.714483259269, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 452.70001220703125, - "height": 45, - "seed": 941916831, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761670500, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "libp2p message go routine", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "libp2p message go routine", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "arrow", - "version": 223, - "versionNonce": 1529614865, - "isDeleted": false, - "id": "B7VNdNs-llWc3PRyhHydC", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2577.1071428571418, - "y": 8229.714483259268, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 38.57142857142844, - "height": 0, - "seed": 2991839, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761609679, - "link": null, - "locked": false, - "startBinding": { - "elementId": "ND1xIwz8NVyMmM6Y8NSnm", - "focus": 0.14893617021276595, - "gap": 3.177659835713314 - }, - "endBinding": { - "elementId": "ToXQc6u63lfDm-D69YBL7", - "focus": 0.013937282229975291, - "gap": 1.8065291210918133 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 38.57142857142844, - 0 - ] - ] - }, - { - "type": "arrow", - "version": 169, - "versionNonce": 1008081905, - "isDeleted": false, - "id": "8g-SasWxTFTmExr5Lvtdq", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3062.821428571427, - "y": 8228.285911830697, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 255.71428571428578, - "height": 7.142857142856883, - "seed": 850621183, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761990666, - "link": null, - "locked": false, - "startBinding": { - "elementId": "ToXQc6u63lfDm-D69YBL7", - "focus": 0.032797490528934076, - "gap": 1 - }, - "endBinding": { - "elementId": "u8EFoC648bJAWgHoFXslU", - "focus": 0.01346599114821577, - "gap": 11.913035820826565 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 255.71428571428578, - -7.142857142856883 - ] - ] - }, - { - "type": "arrow", - "version": 274, - "versionNonce": 1938616753, - "isDeleted": false, - "id": "xo8KLh4h1KQPyMzDLUE81", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3621.3928571428555, - "y": 8228.285911830697, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 71.4285714285711, - "height": 1.4285714285715585, - "seed": 116046623, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761671121, - "link": null, - "locked": false, - "startBinding": { - "elementId": "u8EFoC648bJAWgHoFXslU", - "focus": 0.19122632900180372, - "gap": 2.5959097164464424 - }, - "endBinding": { - "elementId": "6Qv6TKFjYt30YdgNZ--xo", - "focus": -0.05787587937050443, - "gap": 1 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 71.4285714285711, - -1.4285714285715585 - ] - ] - }, - { - "type": "arrow", - "version": 453, - "versionNonce": 1027969937, - "isDeleted": false, - "id": "4WmrbB0O7PJx2miJ_VLWw", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4057.6114154196744, - "y": 8216.364759111899, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 91.04698961158874, - "height": 1.5913794892676378, - "seed": 1094975295, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761671122, - "link": null, - "locked": false, - "startBinding": { - "elementId": "i7o2k0raxYxdIn4R0bDAG", - "focus": 1.6327787205071913, - "gap": 15.935990138343186 - }, - "endBinding": { - "elementId": "GT8b38cLKWR03P4bbdv8D", - "focus": 0.12580355396321918, - "gap": 1 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 91.04698961158874, - -1.5913794892676378 - ] - ] - }, - { - "type": "text", - "version": 110, - "versionNonce": 984819007, - "isDeleted": false, - "id": "i7o2k0raxYxdIn4R0bDAG", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4057.1749992370583, - "y": 8155.428768973556, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 74.1500015258789, - "height": 45, - "seed": 1142931295, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [ - { - "id": "4WmrbB0O7PJx2miJ_VLWw", - "type": "arrow" - } - ], - "updated": 1687761670500, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "wait", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "wait", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "arrow", - "version": 680, - "versionNonce": 1867446641, - "isDeleted": false, - "id": "OfHxFKz9aNsAZISEq1rT2", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4414.577552398095, - "y": 8226.59984499695, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 61.484848042032354, - "height": 4.2296687709122125, - "seed": 1372486527, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761671122, - "link": null, - "locked": false, - "startBinding": { - "elementId": "GT8b38cLKWR03P4bbdv8D", - "focus": 0.04851452014749739, - "gap": 6.712029054568859 - }, - "endBinding": { - "elementId": "w2yQ4eE1Fcolaree2tnQp", - "focus": -0.019773989713656796, - "gap": 1 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 61.484848042032354, - -4.2296687709122125 - ] - ] - }, - { - "type": "ellipse", - "version": 999, - "versionNonce": 1349344511, - "isDeleted": false, - "id": "w2yQ4eE1Fcolaree2tnQp", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4476.39285714286, - "y": 8109.214483259269, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 250, - "height": 205, - "seed": 244364191, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "T356IuXxtISywPDC776KV" - }, - { - "id": "OfHxFKz9aNsAZISEq1rT2", - "type": "arrow" - }, - { - "id": "nF_gkqMcktV2xD9QCN5Lh", - "type": "arrow" - }, - { - "id": "4oUS4LDHmDgJGkY4gGcEW", - "type": "arrow" - } - ], - "updated": 1687761700203, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 1564, - "versionNonce": 1781277055, - "isDeleted": false, - "id": "T356IuXxtISywPDC776KV", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4518.662842319248, - "y": 8144.236038187648, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 165.68333435058594, - "height": 135, - "seed": 1117185983, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761670500, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "remove \nscheduled\ntimeout", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "w2yQ4eE1Fcolaree2tnQp", - "originalText": "remove scheduled timeout", - "lineHeight": 1.25, - "baseline": 122 - }, - { - "type": "arrow", - "version": 271, - "versionNonce": 1257284433, - "isDeleted": false, - "id": "nF_gkqMcktV2xD9QCN5Lh", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 4728.535714285715, - "y": 8216.857340402126, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 52.85714285714312, - "height": 0, - "seed": 1543608287, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761671122, - "link": null, - "locked": false, - "startBinding": { - "elementId": "w2yQ4eE1Fcolaree2tnQp", - "focus": 0.05017421602787204, - "gap": 2.2961065289517677 - }, - "endBinding": { - "elementId": "cWRoM9NnvU2lEFYMK2sBY", - "focus": -0.03398831651620337, - "gap": 3.6377485139078374 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 52.85714285714312, - 0 - ] - ] - }, - { - "id": "G1xcT5JIEubMwfZombxse", - "type": "arrow", - "x": 2834.607142857143, - "y": 6632.285714285712, - "width": 630, - "height": 1524.2857142857138, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1388020767, - "version": 265, - "versionNonce": 266550527, - "isDeleted": false, - "boundElements": null, - "updated": 1687761651440, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -552.8571428571427, - 327.1428571428569 - ], - [ - -630, - 1301.4285714285716 - ], - [ - -530, - 1524.2857142857138 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": -0.23544356160633156, - "gap": 6.186407277752714 - }, - "endBinding": { - "elementId": "ND1xIwz8NVyMmM6Y8NSnm", - "focus": -0.5216948505676587, - "gap": 11.896766697244416 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "t7mkkCH-ZIPx03uflx8vI", - "type": "text", - "x": 2233.826189586094, - "y": 8036.571428571426, - "width": 50.13333511352539, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 2140226847, - "version": 12, - "versionNonce": 1375874961, - "isDeleted": false, - "boundElements": null, - "updated": 1687761663490, - "link": null, - "locked": false, - "text": "(4)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(4)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "KSRSCJu8mbbshHxyt3ooP", - "type": "arrow", - "x": 3786.035714285714, - "y": 8150.8571428571395, - "width": 155.71428571428578, - "height": 391.42857142857065, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 451105233, - "version": 35, - "versionNonce": 927600639, - "isDeleted": false, - "boundElements": null, - "updated": 1687761864140, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -155.71428571428578, - -391.42857142857065 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "6Qv6TKFjYt30YdgNZ--xo", - "focus": -0.33063619342273365, - "gap": 6.4271081449061 - }, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "4oUS4LDHmDgJGkY4gGcEW", - "type": "arrow", - "x": 4570.321428571428, - "y": 8100.8571428571395, - "width": 267.1428571428578, - "height": 374.28571428571377, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 127891551, - "version": 28, - "versionNonce": 2119682609, - "isDeleted": false, - "boundElements": null, - "updated": 1687761868065, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -267.1428571428578, - -374.28571428571377 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "w2yQ4eE1Fcolaree2tnQp", - "focus": 0.3317704306826117, - "gap": 11.344209291327914 - }, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "1gwjmSrfL7LWgA1nJ4Y9j", - "type": "arrow", - "x": 4943.178571428572, - "y": 8065.142857142854, - "width": 315.71428571428623, - "height": 348.57142857142935, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1029846641, - "version": 37, - "versionNonce": 1740078655, - "isDeleted": false, - "boundElements": null, - "updated": 1687761872092, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -315.71428571428623, - -348.57142857142935 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "cWRoM9NnvU2lEFYMK2sBY", - "focus": 1.027968068781049, - "gap": 19.581603537775408 - }, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "type": "line", - "version": 76, - "versionNonce": 625829855, - "isDeleted": false, - "id": "9h5WT_3psSYnf0_axVpGX", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1899.7128063737018, - "y": 8516.928571428569, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 77.14285714285711, - "height": 1.4285714285715585, - "seed": 102419921, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761728926, - "link": null, - "locked": false, - "startBinding": null, - "endBinding": null, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": null, - "points": [ - [ - 0, - 0 - ], - [ - 77.14285714285711, - -1.4285714285715585 - ] - ] - }, - { - "type": "text", - "version": 120, - "versionNonce": 2010965425, - "isDeleted": false, - "id": "qatFwIWRo1K72nCr-6kF0", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1582.1806604648011, - "y": 8492.642857142855, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 269.3500061035156, - "height": 45, - "seed": 1443031985, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761736191, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "timeout query 1", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "timeout query 1", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "type": "ellipse", - "version": 411, - "versionNonce": 530545535, - "isDeleted": false, - "id": "-l8O642ejqMPAKB2g2uy1", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2248.178571428572, - "y": 8474.142857142855, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 373, - "height": 141, - "seed": 997223281, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "8k955FNkkVQJFZaU2Fh1I" - }, - { - "id": "sn4QBd1uAVqcdn2Zp6Smv", - "type": "arrow" - }, - { - "id": "ijajb7Ynw7Ml7TDC-fxoc", - "type": "arrow" - } - ], - "updated": 1687761817801, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 578, - "versionNonce": 1816617937, - "isDeleted": false, - "id": "8k955FNkkVQJFZaU2Fh1I", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2321.378153685523, - "y": 8499.791829069203, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 226.85000610351562, - "height": 90, - "seed": 907269457, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761788064, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "worker picks \nup timeout 1", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "-l8O642ejqMPAKB2g2uy1", - "originalText": "worker picks up timeout 1", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "ellipse", - "version": 272, - "versionNonce": 445282303, - "isDeleted": false, - "id": "_2iFjvxDCqXzn8NjdFps3", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2696.678571428572, - "y": 8413.499999999996, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 322, - "height": 269, - "seed": 1081017137, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "nxKTCb5JK_VUIuQR_EQ3U" - }, - { - "id": "sn4QBd1uAVqcdn2Zp6Smv", - "type": "arrow" - }, - { - "id": "uqINunrryfFmy4ntH-ZDF", - "type": "arrow" - } - ], - "updated": 1687761778146, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 367, - "versionNonce": 58215281, - "isDeleted": false, - "id": "nxKTCb5JK_VUIuQR_EQ3U", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2750.78437660578, - "y": 8457.894137930405, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 214.10000610351562, - "height": 180, - "seed": 384398609, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761778146, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "update \nrouting \ntable and \nquery state", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "_2iFjvxDCqXzn8NjdFps3", - "originalText": "update routing table and query state", - "lineHeight": 1.25, - "baseline": 167 - }, - { - "type": "ellipse", - "version": 459, - "versionNonce": 1451571185, - "isDeleted": false, - "id": "IfRcHq9sD-6DD5uDrYBsv", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3057.321428571429, - "y": 8475.85714285714, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 258, - "height": 141, - "seed": 2096802545, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "ISoxB2vLSgSrHnYnk_yBK" - }, - { - "id": "uqINunrryfFmy4ntH-ZDF", - "type": "arrow" - }, - { - "id": "05mMaAGbop9meInlHhF5U", - "type": "arrow" - } - ], - "updated": 1687761854033, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 640, - "versionNonce": 1034111231, - "isDeleted": false, - "id": "ISoxB2vLSgSrHnYnk_yBK", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3108.871324025415, - "y": 8501.506114783488, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 154.46665954589844, - "height": 90, - "seed": 1529488593, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761780198, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "enqueue \nquery 3", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "IfRcHq9sD-6DD5uDrYBsv", - "originalText": "enqueue query 3", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "arrow", - "version": 185, - "versionNonce": 1111885009, - "isDeleted": false, - "id": "sn4QBd1uAVqcdn2Zp6Smv", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2629.7946608034886, - "y": 8548.649933304065, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 59.726222106084606, - "height": 3.0746342110669502, - "seed": 487547569, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761778175, - "link": null, - "locked": false, - "startBinding": { - "elementId": "-l8O642ejqMPAKB2g2uy1", - "focus": 0.19748806116220788, - "gap": 8.842543029714193 - }, - "endBinding": { - "elementId": "_2iFjvxDCqXzn8NjdFps3", - "focus": 0.08223254767199097, - "gap": 7.182280315794031 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 59.726222106084606, - -3.0746342110669502 - ] - ] - }, - { - "type": "arrow", - "version": 175, - "versionNonce": 1974420145, - "isDeleted": false, - "id": "uqINunrryfFmy4ntH-ZDF", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3020.964285714286, - "y": 8544.928571428569, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 34.285714285714675, - "height": 4.2857142857146755, - "seed": 1330766993, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1687761778175, - "link": null, - "locked": false, - "startBinding": { - "elementId": "_2iFjvxDCqXzn8NjdFps3", - "focus": -0.1726662429761219, - "gap": 2.3268467986684698 - }, - "endBinding": { - "elementId": "IfRcHq9sD-6DD5uDrYBsv", - "focus": -0.2660525213513878, - "gap": 2.1717539067364555 - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, - 0 - ], - [ - 34.285714285714675, - 4.2857142857146755 - ] - ] - }, - { - "type": "text", - "version": 80, - "versionNonce": 1351957983, - "isDeleted": false, - "id": "0ZSPpnuO3b71xtu59JNaQ", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2330.57500076294, - "y": 8366.357142857141, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 49.349998474121094, - "height": 45, - "seed": 2108238449, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761840128, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "(5)", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "(5)", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "id": "ijajb7Ynw7Ml7TDC-fxoc", - "type": "arrow", - "x": 2824.607142857143, - "y": 6625.142857142855, - "width": 652.8571428571422, - "height": 1844.2857142857138, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 908902833, - "version": 234, - "versionNonce": 1739537599, - "isDeleted": false, - "boundElements": null, - "updated": 1687761834352, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -548.5714285714284, - 300 - ], - [ - -652.8571428571422, - 1181.4285714285697 - ], - [ - -587.1428571428573, - 1619.9999999999982 - ], - [ - -474.2857142857142, - 1844.2857142857138 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": -0.11814747752245933, - "gap": 8.422049414084036 - }, - "endBinding": { - "elementId": "-l8O642ejqMPAKB2g2uy1", - "focus": -0.24499324709220285, - "gap": 12.124706240236733 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "05mMaAGbop9meInlHhF5U", - "type": "arrow", - "x": 3176.035714285714, - "y": 8462.285714285712, - "width": 194.0515604336274, - "height": 750.8410548975844, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1611672063, - "version": 79, - "versionNonce": 116736401, - "isDeleted": false, - "boundElements": null, - "updated": 1687762256900, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 150, - -385.71428571428623 - ], - [ - 194.0515604336274, - -750.8410548975844 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "IfRcHq9sD-6DD5uDrYBsv", - "focus": -0.32590036097115604, - "gap": 13.783457338649924 - }, - "endBinding": { - "elementId": "LhBYl9Ck9WBc0wwqjSC77", - "focus": -0.9567340710924729, - "gap": 14.640550305746842 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "type": "ellipse", - "version": 433, - "versionNonce": 1473808913, - "isDeleted": false, - "id": "5DUnvo6T79NX5T99XuZym", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2216.678571428571, - "y": 8740.357142857143, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 373, - "height": 141, - "seed": 1360376159, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [ - { - "type": "text", - "id": "PKGPzQjgx_f49egQy5s6x" - }, - { - "id": "sYzsV-FDrYb9P9wb19_97", - "type": "arrow" - } - ], - "updated": 1687761918675, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 604, - "versionNonce": 307176113, - "isDeleted": false, - "id": "PKGPzQjgx_f49egQy5s6x", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 2388.503156546545, - "y": 8788.506114783491, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 29.600000381469727, - "height": 45, - "seed": 1865821567, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687761899621, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "...", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "5DUnvo6T79NX5T99XuZym", - "originalText": "...", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "id": "sYzsV-FDrYb9P9wb19_97", - "type": "arrow", - "x": 2816.035714285714, - "y": 6615.142857142855, - "width": 658.5714285714284, - "height": 2135.7142857142844, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 110732433, - "version": 205, - "versionNonce": 1478860145, - "isDeleted": false, - "boundElements": null, - "updated": 1687761935301, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -562.8571428571427, - 318.57142857142753 - ], - [ - -658.5714285714284, - 1274.285714285712 - ], - [ - -547.1428571428569, - 2135.7142857142844 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": 0.06030758581521888, - "gap": 7.6456849423036815 - }, - "endBinding": { - "elementId": "5DUnvo6T79NX5T99XuZym", - "focus": -0.677606072054818, - "gap": 10.34955969685609 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "mMpACUVyOuKjmZ0X3IENa", - "type": "text", - "x": 2269.5404753003804, - "y": 8656.571428571426, - "width": 50.13333511352539, - "height": 45, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 693290833, - "version": 9, - "versionNonce": 817304753, - "isDeleted": false, - "boundElements": null, - "updated": 1687761945807, - "link": null, - "locked": false, - "text": "(6)", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 32, - "containerId": null, - "originalText": "(6)", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "id": "xOhUG0KRKng0aYf6Fv99u", - "type": "text", - "x": 3248.564284188407, - "y": 7283.7142857142835, - "width": 117.80000305175781, - "height": 90, - "angle": 0, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1739552209, - "version": 71, - "versionNonce": 1786940337, - "isDeleted": false, - "boundElements": null, - "updated": 1687762029500, - "link": null, - "locked": false, - "text": "new\nthread", - "fontSize": 36, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 77, - "containerId": null, - "originalText": "new\nthread", - "lineHeight": 1.25, - "isFrameName": false - }, - { - "type": "text", - "version": 111, - "versionNonce": 1445352561, - "isDeleted": false, - "id": "y1-oblJF1luVGse4i18K4", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3209.9928556169784, - "y": 6877.285714285713, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 117.80000305175781, - "height": 90, - "seed": 1314668511, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687762027777, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "new\nthread", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "new\nthread", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "type": "text", - "version": 82, - "versionNonce": 1178864991, - "isDeleted": false, - "id": "dxUa-ovhV0Cjls_SycfvE", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 3098.564284188407, - "y": 8117.285714285711, - "strokeColor": "#1971c2", - "backgroundColor": "transparent", - "width": 117.80000305175781, - "height": 90, - "seed": 2138320895, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687762034615, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "new\nthread", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "new\nthread", - "lineHeight": 1.25, - "baseline": 77 - }, - { - "id": "hRgPhDd2QSKt5dg4jmNs1", - "type": "arrow", - "x": 3164.6071428571427, - "y": 6955.142857142855, - "width": 264.2857142857142, - "height": 288.57142857142935, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1715014015, - "version": 97, - "versionNonce": 682452433, - "isDeleted": false, - "boundElements": null, - "updated": 1687762084959, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -14.28571428571422, - -142.85714285714312 - ], - [ - -264.2857142857142, - -288.57142857142935 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "ChAZ14sZHvqijmnOHV4-2", - "focus": 0.991564777268982, - "gap": 7.683183223567966 - }, - "endBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": 0.8140583702012011, - "gap": 8.998751633977811 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "x8aM4kFjBWtdo1BNX1DDU", - "type": "arrow", - "x": 3184.6071428571427, - "y": 7335.142857142855, - "width": 268.57142857142844, - "height": 688.5714285714294, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 981631921, - "version": 104, - "versionNonce": 658964127, - "isDeleted": false, - "boundElements": null, - "updated": 1687762106267, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -11.428571428571104, - -517.1428571428569 - ], - [ - -268.57142857142844, - -688.5714285714294 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "yTRHZWW47ZRASFsCCGN4M", - "focus": 0.8533722345674807, - "gap": 11.999037805331426 - }, - "endBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": 0.45593573093570355, - "gap": 6.418058621299799 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "gM07ZRTN9g49j8P3nxqmj", - "type": "arrow", - "x": 3260.321428571428, - "y": 7813.7142857142835, - "width": 352.8571428571431, - "height": 1184.2857142857138, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 1414250737, - "version": 155, - "versionNonce": 553203441, - "isDeleted": false, - "boundElements": null, - "updated": 1687762176249, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 27.142857142857338, - -134.28571428571377 - ], - [ - -2.8571428571426623, - -745.7142857142853 - ], - [ - -80, - -1024.2857142857138 - ], - [ - -325.7142857142858, - -1184.2857142857138 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "mVa4UcAqU0qGbffqABfdN", - "focus": 0.7460919801606536, - "gap": 4.450912764352282 - }, - "endBinding": { - "elementId": "2BtTstoyLCGsiY2S7HLuY", - "focus": 0.13471586552981024, - "gap": 7.877336285850738 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "IN-cN5thwLGEuuOPzckqg", - "type": "arrow", - "x": 3030.3214285714284, - "y": 8163.714285714283, - "width": 301.28918049442245, - "height": 438.48883302829745, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "seed": 393687249, - "version": 126, - "versionNonce": 283868465, - "isDeleted": false, - "boundElements": null, - "updated": 1687762268302, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 272.8571428571431, - -232.85714285714312 - ], - [ - 301.28918049442245, - -438.48883302829745 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "ToXQc6u63lfDm-D69YBL7", - "focus": 0.4403657133342141, - "gap": 11.677474191568194 - }, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "type": "text", - "version": 302, - "versionNonce": 734610769, - "isDeleted": false, - "id": "-zxScViLWjfE7nnmG-5iv", - "fillStyle": "hachure", - "strokeWidth": 4, - "strokeStyle": "dashed", - "roughness": 1, - "opacity": 100, - "angle": 5.879629346976776, - "x": 2180.4059513637, - "y": 6678.357142857141, - "strokeColor": "#e03131", - "backgroundColor": "transparent", - "width": 414.1166687011719, - "height": 45, - "seed": 539010289, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1687762307475, - "link": null, - "locked": false, - "fontSize": 36, - "fontFamily": 1, - "text": "single worker go routine", - "textAlign": "center", - "verticalAlign": "top", - "containerId": null, - "originalText": "single worker go routine", - "lineHeight": 1.25, - "baseline": 32 - }, - { - "id": "UnRuHOI5kZJefynVHzSKI", - "type": "text", - "x": 2988.4928632463734, - "y": 5772.999999999999, - "width": 362.23333740234375, - "height": 140, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "roundness": null, - "seed": 1922298303, - "version": 106, - "versionNonce": 743568223, - "isDeleted": false, - "boundElements": null, - "updated": 1687762542558, - "link": null, - "locked": false, - "text": "* action 45b7 (timeout 0)\n* action ee67 (timeout 1)\n* action a123 (timeout 2)\n* action bc49 (timeout 3)", - "fontSize": 28, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 130, - "containerId": null, - "originalText": "* action 45b7 (timeout 0)\n* action ee67 (timeout 1)\n* action a123 (timeout 2)\n* action bc49 (timeout 3)", - "lineHeight": 1.25, - "isFrameName": false - } - ], - "appState": { - "gridSize": null, - "viewBackgroundColor": "#ffffff" - }, - "files": {} -} \ No newline at end of file diff --git a/design/excalidraw/query-process.png b/design/excalidraw/query-process.png deleted file mode 100644 index 2c541f3..0000000 Binary files a/design/excalidraw/query-process.png and /dev/null differ diff --git a/design/excalidraw/query-run.png b/design/excalidraw/query-run.png deleted file mode 100644 index e4d0029..0000000 Binary files a/design/excalidraw/query-run.png and /dev/null differ diff --git a/design/excalidraw/query-setup.png b/design/excalidraw/query-setup.png deleted file mode 100644 index 8f4c029..0000000 Binary files a/design/excalidraw/query-setup.png and /dev/null differ diff --git a/design/excalidraw/scheduler.png b/design/excalidraw/scheduler.png deleted file mode 100644 index bfc13eb..0000000 Binary files a/design/excalidraw/scheduler.png and /dev/null differ diff --git a/design/excalidraw/single-thread-state-machine.png b/design/excalidraw/single-thread-state-machine.png deleted file mode 100644 index c2e27ad..0000000 Binary files a/design/excalidraw/single-thread-state-machine.png and /dev/null differ diff --git a/design/excalidraw/states.png b/design/excalidraw/states.png deleted file mode 100644 index 7fb65f9..0000000 Binary files a/design/excalidraw/states.png and /dev/null differ diff --git a/design/statemachine.md b/design/statemachine.md deleted file mode 100644 index 51b1a86..0000000 --- a/design/statemachine.md +++ /dev/null @@ -1,301 +0,0 @@ -# State Machine Rationale and Design - -Author: [Ian Davis](https://github.com/iand) - -Last Updated: 2023-07-31 - -This document descriibes the design and rationale of the state machine approach to implementing the Kademlia protocol. - -## Rationale - -The main non-functional design goal of this new Kademlia implementation is to create a more predictable, maintainable and testable codebase that can provide an extensible platform for new DHT capabilities. As OSS it is important that new contributors are attracted to the project so ease of understanding, straightforward debugging and consistency of design are also high priority. - -The hierarchical state machine approach addresses these goals in the following ways: - - - **Determinism** - The state machine follows a deterministic flow, meaning that given a particular input or set of conditions, it will always produce the same output. This predictability makes it easier to isolate and reproduce issues, as bugs are more likely to occur consistently within specific states or transitions. The events in the state machines can be linearised, in the sense that the same sequence of events will produce the same sequence of state transitions, regardless of the timing of event arrival. When combined with a determistic clock implementation this enables accurate simulation of time-dependent behaviours, such as timeouts. - - - **Transparency** - Each state in the machine is assigned a specific responsibility, focusing in on a particular aspect of the protocol's behaviour. This focused scope narrows down the search for potential problems to specific areas of the codebase, reducing the debugging space and making it more manageable. The states are represented as simple data values that can be easily inspected or logged which provides developers with clear visibility into the state transitions and the current state of the protocol during its execution. Developers can track the sequence of events leading to an issue, enabling them to identify root causes and simplifying resolution. - - - **Testability** - The state machines can be unit tested by testing each state and transition separately. This approach helps ensure that each component functions correctly, leading to a more reliable and robust protocol implementation. When debugging a specific state, developers can isolate that state's context, ignoring the complexities of other parts of the code. This isolation allows for focused testing and simplifies the understanding of how individual states contribute to the overall behavior. A state machine can be placed in a specific initial state and tested for expected behaviour under various valid and invalid inputs. - - - **Extensibility** - A state machine is inherently easier to extend due to its modular and hierarchical structure. Each state in the machine represents a specific behaviour or functionality, making it simpler to add new states and transitions to accommodate additional features or enhancements. The clear separation of concerns, where each state focuses on a specific aspect of the software's behaviour, makes it easier to identify the appropriate location for incorporating new features, reducing the risk of unintended interactions with existing states. - -- **Consistency and Understandability** - The hierarchical nature of the state machines promotes readability and understandability of the code. New states are logically integrated into the existing structure, making it easier for developers to comprehend the software's behaviour as a whole. The transitions between states can be drawn to help visualise behaviours. The consistent use of the state machine pattern across components ensures uniformity in behavior and interface, allowing developers to understand the system more quickly and reducing ambiguity. - -In addition, the state machine model enforces a structured and controlled execution flow, making it easier to impose limits on resource consumption: - - - **Memory** - As the state machines transition between states they can release memory allocated for states that are no longer active. - - **Concurrency** - The state machines provide bounds on concurrency, offering full control over the number of in-flight operations. - - **CPU** - The finite number of states and combinations ensure that the work to be undertaken is bounded and known ahead of time. - - **Network** - Network connections are opened, closed, and reused appropriately based on the state transitions. This controlled socket management can help prevent leaks or exhaustion, leading to more efficient utilization of network resources. - -## Components - -### The Coordinator (`coord` package) - -Kademlia is inherently an asynchronous protocol dependent on variability of network latency and responsiveness of peers. -We shouldn't hide this complexity from users of go-kademlia but we can confine its API surface. - -The `Coordinator` plays this role. Users of the `Coordinator` subscribe to a channel of Kademlia events to receive notificationof the progress of queries as well as other things including updates to routing tables. -Users call methods on the Coordinator to initiate operations such a starting a query. They receive an error response immediately if the operation could not be started, perhaps due to invalid arguments passed by the caller. Otherwise, if no error is returned, the caller must wait for events on the channel to monitor the outcome of their request. - -A pattern for using this is in the `statemachine` example. The `IpfsDht` type in that example implements a -synchronous `FindNode` method that starts a query via the coordinator and then monitors the event channel -for updates as the query progresses, until it finally completes. - -### The Pool and Query state machines (`query` package) - -Queries are operated via two state machines organised hierarchically. The query `Pool` state machine manages the lifetimes of -any number of `Query` state machines. Each user-submitted query is added to the `Pool` and is executed by a -dedicated `Query`. - -The `Query` state machine manages the lifecycle of the query, traversing nodes according to an iteration strategy. -By default this is `ClosestNodesIter` which maintains a dynamic list of nodes and their connection status, sorted by ascending distance to the target of the query. The query is configured with a message to be delivered successfuully to a threshold number of nodes, typically 20. - -The query proceeds by iterating through the list, attempting to communicate with the first uncontacted node in the iterator, which in the default case is the next closest node to the target. -The query signals this by entering the `waiting to send message` state. The query marks the node as waiting for a response. - -When polled to advance to the next state the query will pick the next node in the iterator and enter the `waiting to send message` state for this node, repeating with each poll until it reaches a configured concurrency limit. At that point it enters the `waiting at capacity` state, indicating that it will not issue any further message requests until capacity is available. - -Receiving a response from a node may free up capacity, causing the query to proceed to the next node or, if no uncontacted nodes remain, enter the `waiting with capacity` state. -Responses received from nodes may contain additional closer node candidates which the query will add to the iterator for possible traversal. -The query enters the `finished` state when it has received successful responses from at least the threshold number of nodes, in iteration order, or has run out of nodes to contact. - -Resources are bounded by limiting the number of concurrent queries that can be running and the number of concurrent messages that can be in-flight for each query. Capacity is freed when an in-flight message receives a successful response, a communication failure or reaches an inactivity deadline. The `Pool` prioritises work for queries that are making progress. - - - -## Operation - -### Simple Query - -The following diagram shows the sequence of operations involved in performaing a simple query. It illustrates the separation of responsibilities between the Coordinator, the Pool and Query state machines and the Endpoint. - -Asynchronous operations are shown as dotted lines with open arrows. Notably the `Coordinator` turns synchronous calls such as `StartQuery` into asynchronous events (`eventAddQuery` in this case) which are then queued and processed internally in the sequence they were received. Requests to the `Endpoint` are made asynchronously and responses are added to the internal queue. - -Interactions with the `Pool` and `Query` state machines are synchronous and deterministic. - -```mermaid -sequenceDiagram - - participant Caller - participant Endpoint - participant Coordinator - participant Pool - participant Query - - Caller ->> Coordinator : StartQuery - activate Caller - activate Coordinator - Coordinator --) Coordinator: eventAddQuery - Coordinator ->> Pool: Advance:EventPoolAddQuery - activate Pool - Pool ->> Query: Advance:EventQueryPoll - activate Query - Query ->> Pool: StateQueryWaitingMessage - deactivate Query - Pool ->> Coordinator: StatePoolQueryMessage - deactivate Pool - Coordinator -->> Endpoint:SendMessage - activate Endpoint - Endpoint -->> Coordinator:Response - deactivate Endpoint - Coordinator --) Coordinator: eventMessageResponse - Coordinator --) Caller: KademliaOutboundQueryProgressedEvent - Coordinator ->> Pool: Advance:EventPoolMessageResponse - activate Pool - Pool ->> Query: Advance:EventQueryMessageResponse - activate Query - Query ->> Pool: StateQueryWaitingMessage - deactivate Query - Pool ->> Coordinator: StatePoolQueryMessage - deactivate Pool - Coordinator -->> Endpoint:SendMessage - activate Endpoint - Endpoint -->> Coordinator:Response - deactivate Endpoint - Coordinator --) Coordinator: eventMessageResponse - Coordinator --) Caller: KademliaOutboundQueryProgressedEvent - Coordinator ->> Pool: Advance:EventPoolMessageResponse - activate Pool - Pool ->> Query: Advance:EventQueryMessageResponse - activate Query - Query ->> Pool: StateQueryFinished - deactivate Query - Pool ->> Coordinator: StatePoolQueryFinished - deactivate Pool - Coordinator --) Caller: KademliaOutboundQueryFinishedEvent - deactivate Coordinator - deactivate Caller -``` -### Query with higher request concurrency - -The following diagram illustrates the interactions with a query `Pool` that allows several concurrent queries. Queries are limited to a request concurrency of 3. - -The `Caller` starts a query which is added to the `Pool` state machine, creating a new `Query`. The `Query` state machine requests to send a message to the first node in its iterator by entering a state of `waiting for a message`. This is conveyed by the `Pool` to the `Coordinator` which instructs the `Endpoint` to send the message. - -The `Coordinator` then polls the `Pool` to determine if it can proceed with work. The `Pool` polls the `Query` which has not reached request capacity so can issue a new request. It responding by entering the `waiting for a message` state which contains the next node that the query needs to contact. - -This is repeated, with the `Query` requesting to send a third message. It has now reached its configured request concurrency so on the next poll by the `Pool` it enters the state `waiting at capacity` to signal that it cannot progress until responses are received (or deadlines are reached). - -Since the `Pool` has capacity to manage more than one query it enters the state `waiting with capacity`. - -As responses are received, and assuming no further closer nodes are discovered, the `Query` enters the state of `waiting with capacity`, indicating that it could start more work if new nodes were found. - -```mermaid -sequenceDiagram - - participant Caller - participant Endpoint - participant Coordinator - participant Pool - participant Query - - Caller ->> Coordinator : StartQuery - activate Caller - activate Coordinator - Coordinator --) Coordinator: eventAddQuery - Coordinator ->> Pool: Advance:EventPoolAddQuery - activate Pool - Pool ->> Query: Advance:EventQueryPoll - activate Query - Query ->> Pool: StateQueryWaitingMessage - deactivate Query - Pool ->> Coordinator: StatePoolQueryMessage - deactivate Pool - Coordinator -->> Endpoint:SendMessage:1 - activate Endpoint - - Coordinator ->> Pool: Advance:EventPoolPoll - activate Pool - Pool ->> Query: Advance:EventQueryPoll - activate Query - Query ->> Pool: StateQueryWaitingMessage - deactivate Query - Pool ->> Coordinator: StatePoolQueryMessage - deactivate Pool - Coordinator -->> Endpoint:SendMessage:2 - - Coordinator ->> Pool: Advance:EventPoolPoll - activate Pool - Pool ->> Query: Advance:EventQueryPoll - activate Query - Query ->> Pool: StateQueryWaitingMessage - deactivate Query - Pool ->> Coordinator: StatePoolQueryMessage - deactivate Pool - Coordinator -->> Endpoint:SendMessage:3 - - Coordinator ->> Pool: Advance:EventPoolPoll - activate Pool - Pool ->> Query: Advance:EventQueryPoll - activate Query - Query ->> Pool: StateQueryWaitingAtCapacity - deactivate Query - Pool ->> Coordinator: StatePoolWaitingWithCapacity - deactivate Pool - - - Endpoint -->> Coordinator:Response:1 - Coordinator --) Coordinator: eventMessageResponse - Coordinator --) Caller: KademliaOutboundQueryProgressedEvent:1 - - Coordinator ->> Pool: Advance:EventPoolMessageResponse:1 - activate Pool - Pool ->> Query: Advance:EventQueryMessageResponse:1 - activate Query - Query ->> Pool: StateQueryWaitingWithCapacity - deactivate Query - Pool ->> Coordinator: StatePoolWaitingWithCapacity - deactivate Pool - - Endpoint -->> Coordinator:Response:2 - deactivate Endpoint - Coordinator --) Coordinator: eventMessageResponse - Coordinator --) Caller: KademliaOutboundQueryProgressedEvent:2 - - Coordinator ->> Pool: Advance:EventPoolMessageResponse:2 - activate Pool - Pool ->> Query: Advance:EventQueryMessageResponse:2 - activate Query - Query ->> Pool: StateQueryWaitingWithCapacity - deactivate Query - Pool ->> Coordinator: StatePoolWaitingWithCapacity - deactivate Pool -``` - -### Two concurrent queries - -The following diagram shows the interactions involved when two queries are started. The `Caller` should expect to receive progress events from the `Coordinator` for both queries. These events may be interleaved depending on the order in which responses are received from the network. - -```mermaid -sequenceDiagram - - participant Caller - participant Endpoint - participant Coordinator - participant Pool - participant Query1 - participant Query2 - - Caller ->> Coordinator : StartQuery - activate Caller - activate Coordinator - Coordinator --) Coordinator: eventAddQuery - Caller ->> Coordinator : StartQuery - Coordinator --) Coordinator: eventAddQuery - Coordinator ->> Pool: Advance:EventPoolAddQuery:1 - activate Pool - Pool ->> Query1: Advance:EventQueryPoll - activate Query1 - Query1 ->> Pool: StateQueryWaitingMessage - deactivate Query1 - Pool ->> Coordinator: StatePoolQueryMessage - deactivate Pool - Coordinator -->> Endpoint:SendMessage:1 - activate Endpoint - - - - Coordinator ->> Pool: Advance:EventPoolAddQuery:2 - activate Pool - Pool ->> Query2: Advance:EventQueryPoll - activate Query2 - Query2 ->> Pool: StateQueryWaitingMessage - deactivate Query2 - Pool ->> Coordinator: StatePoolQueryMessage - deactivate Pool - Coordinator -->> Endpoint:SendMessage:2 - - - Endpoint -->> Coordinator:Response:1 - Coordinator --) Coordinator: eventMessageResponse - Coordinator --) Caller: KademliaOutboundQueryProgressedEvent:1 - - Coordinator ->> Pool: Advance:EventPoolMessageResponse:1 - activate Pool - Pool ->> Query1: Advance:EventQueryMessageResponse - activate Query1 - Query1 ->> Pool: StateQueryWaitingMessage - deactivate Query1 - Pool ->> Coordinator: StatePoolQueryMessage - deactivate Pool - - Endpoint -->> Coordinator:Response:2 - deactivate Endpoint - Coordinator --) Coordinator: eventMessageResponse - Coordinator --) Caller: KademliaOutboundQueryProgressedEvent:2 - - Coordinator ->> Pool: Advance:EventPoolMessageResponse:2 - activate Pool - Pool ->> Query2: Advance:EventQueryMessageResponse - activate Query2 - Query2 ->> Pool: StateQueryWaitingMessage - deactivate Query2 - Pool ->> Coordinator: StatePoolQueryMessage - deactivate Pool -``` - - - - diff --git a/event/action.go b/event/action.go deleted file mode 100644 index 3eaeeab..0000000 --- a/event/action.go +++ /dev/null @@ -1,9 +0,0 @@ -package event - -import "context" - -// Action is an interface for an action that can be run. It is used as unit by -// the scheduler (event queue + planner) -type Action interface { - Run(context.Context) -} diff --git a/event/basicaction.go b/event/basicaction.go deleted file mode 100644 index 409cf5d..0000000 --- a/event/basicaction.go +++ /dev/null @@ -1,15 +0,0 @@ -package event - -import ( - "context" -) - -// A BasicAction is the default Action used for event scheduling in the Kademlia implementation. -type BasicAction func(context.Context) - -var _ Action = (*BasicAction)(nil) - -// Run executes the action -func (a BasicAction) Run(ctx context.Context) { - a(ctx) -} diff --git a/event/basicaction_test.go b/event/basicaction_test.go deleted file mode 100644 index 9efc10c..0000000 --- a/event/basicaction_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package event - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestBasicAction(t *testing.T) { - b := false - ba := BasicAction(func(ctx context.Context) { b = true }) - ba.Run(context.Background()) - require.True(t, b) -} diff --git a/event/chanqueue.go b/event/chanqueue.go deleted file mode 100644 index e32f95a..0000000 --- a/event/chanqueue.go +++ /dev/null @@ -1,54 +0,0 @@ -package event - -import ( - "context" - - "github.com/plprobelab/go-kademlia/util" -) - -// ChanQueue is a trivial queue implementation using a channel -type ChanQueue struct { - queue chan Action -} - -var _ EventQueueWithEmpty = (*ChanQueue)(nil) - -// NewChanQueue creates a new queue -func NewChanQueue(capacity int) *ChanQueue { - return &ChanQueue{ - queue: make(chan Action, capacity), - } -} - -// Enqueue adds an element to the queue -func (q *ChanQueue) Enqueue(ctx context.Context, e Action) { - _, span := util.StartSpan(ctx, "ChanQueue.Enqueue") - defer span.End() - q.queue <- e -} - -// Dequeue reads the next element from the queue, note that this operation is blocking -func (q *ChanQueue) Dequeue(ctx context.Context) Action { - _, span := util.StartSpan(ctx, "ChanQueue.Dequeue") - defer span.End() - - if q.Empty() { - span.AddEvent("empty queue") - return nil - } - - return <-q.queue -} - -// Empty returns true if the queue is empty -func (q *ChanQueue) Empty() bool { - return len(q.queue) == 0 -} - -func (q *ChanQueue) Size() uint { - return uint(len(q.queue)) -} - -func (q *ChanQueue) Close() { - close(q.queue) -} diff --git a/event/chanqueue_test.go b/event/chanqueue_test.go deleted file mode 100644 index 927926b..0000000 --- a/event/chanqueue_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package event - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestChanQueue(t *testing.T) { - ctx := context.Background() - nEvents := 10 - events := make([]Action, nEvents) - for i := 0; i < nEvents; i++ { - events[i] = IntAction(i) - } - - q := NewChanQueue(nEvents) - if q.Size() != 0 { - t.Errorf("Expected size 0, got %d", q.Size()) - } - require.True(t, q.Empty()) - - q.Enqueue(ctx, events[0]) - if q.Size() != 1 { - t.Errorf("Expected size 1, got %d", q.Size()) - } - require.False(t, q.Empty()) - - q.Enqueue(ctx, events[1]) - if q.Size() != 2 { - t.Errorf("Expected size 2, got %d", q.Size()) - } - require.False(t, q.Empty()) - - if !q.Empty() { - e := q.Dequeue(ctx) - require.Equal(t, e, events[0]) - if q.Size() != 1 { - t.Errorf("Expected size 1, got %d", q.Size()) - } - require.False(t, q.Empty()) - } - - if !q.Empty() { - e := q.Dequeue(ctx) - require.Equal(t, e, events[1]) - if q.Size() != 0 { - t.Errorf("Expected size 0, got %d", q.Size()) - } - require.True(t, q.Empty()) - } - - require.Nil(t, q.Dequeue(ctx)) - - q.Close() -} diff --git a/event/doc.go b/event/doc.go deleted file mode 100644 index 1bb60cd..0000000 --- a/event/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package event provides an abstraction for single worker multi threaded applications. Some applications are multi -// threaded by design (e.g Kademlia lookup), but having a sequential execution brings many benefits such as -// deterministic testing, easier debugging, sequential tracing, and sometimes even increased performance. -package event diff --git a/event/planner.go b/event/planner.go deleted file mode 100644 index 90a23f5..0000000 --- a/event/planner.go +++ /dev/null @@ -1,83 +0,0 @@ -package event - -import ( - "context" - "time" -) - -// MaxTime is the maximum time.Time value -var MaxTime = time.Unix(1<<63-62135596801, 999999999) - -// PlannedAction is an interface for actions that are scheduled to run at a -// specific time. -type PlannedAction interface { - // Time returns the time at which the action is scheduled to run - Time() time.Time - // Action returns the action that is scheduled to run - Action() Action -} - -// ActionPlanner is an interface for scheduling actions at a specific time. -type ActionPlanner interface { - // ScheduleAction schedules an action to run at a specific time - ScheduleAction(context.Context, time.Time, Action) PlannedAction - // RemoveAction removes an action from the planner - RemoveAction(context.Context, PlannedAction) bool - - // PopOverdueActions returns all actions that are overdue and removes them - // from the planner - PopOverdueActions(context.Context) []Action -} - -// MultiActionPlanner is an interface for scheduling multiple actions at -// specific times. -type MultiActionPlanner interface { - ActionPlanner - - // ScheduleActions schedules multiple actions at specific times - ScheduleActions(context.Context, []time.Time, []Action) []PlannedAction - // RemoveActions removes multiple actions from the planner - RemoveActions(context.Context, []PlannedAction) -} - -// ScheduleActions schedules multiple actions at specific times using a planner. -func ScheduleActions(ctx context.Context, p ActionPlanner, - times []time.Time, actions []Action, -) []PlannedAction { - if len(times) != len(actions) { - return nil - } - - switch p := p.(type) { - case MultiActionPlanner: - return p.ScheduleActions(ctx, times, actions) - default: - res := make([]PlannedAction, len(times)) - for i, d := range times { - p.ScheduleAction(ctx, d, actions[i]) - } - return res - } -} - -// RemoveActions removes multiple actions from the planner. -func RemoveActions(ctx context.Context, p ActionPlanner, actions []PlannedAction) { - switch p := p.(type) { - case MultiActionPlanner: - p.RemoveActions(ctx, actions) - default: - for _, a := range actions { - p.RemoveAction(ctx, a) - } - } -} - -// AwareActionPlanner is an interface for scheduling actions at a specific time -// and knowing when the next action will be scheduled. -type AwareActionPlanner interface { - ActionPlanner - - // NextActionTime returns the time of the next action that will be - // scheduled. If there are no actions scheduled, it returns MaxTime. - NextActionTime(context.Context) time.Time -} diff --git a/event/queue.go b/event/queue.go deleted file mode 100644 index f24fd65..0000000 --- a/event/queue.go +++ /dev/null @@ -1,81 +0,0 @@ -package event - -import ( - "context" -) - -type EventQueue interface { - Enqueue(context.Context, Action) - Dequeue(context.Context) Action - - Size() uint - Close() -} - -type EventQueueEnqueueMany interface { - EventQueue - EnqueueMany(context.Context, []Action) -} - -func EnqueueMany(ctx context.Context, q EventQueue, actions []Action) { - switch queue := q.(type) { - case EventQueueEnqueueMany: - queue.EnqueueMany(ctx, actions) - default: - for _, a := range actions { - q.Enqueue(ctx, a) - } - } -} - -type EventQueueDequeueMany interface { - DequeueMany(context.Context, int) []Action -} - -func DequeueMany(ctx context.Context, q EventQueue, n int) []Action { - switch queue := q.(type) { - case EventQueueDequeueMany: - return queue.DequeueMany(ctx, n) - default: - actions := make([]Action, 0, n) - for i := 0; i < n; i++ { - if a := q.Dequeue(ctx); a != nil { - actions = append(actions, a) - } else { - break - } - } - return actions - } -} - -type EventQueueDequeueAll interface { - DequeueAll(context.Context) []Action -} - -func DequeueAll(ctx context.Context, q EventQueue) []Action { - switch queue := q.(type) { - case EventQueueDequeueAll: - return queue.DequeueAll(ctx) - default: - actions := make([]Action, 0, q.Size()) - for a := q.Dequeue(ctx); a != nil; a = q.Dequeue(ctx) { - actions = append(actions, a) - } - return actions - } -} - -type EventQueueWithEmpty interface { - EventQueue - Empty() bool -} - -func Empty(q EventQueue) bool { - switch queue := q.(type) { - case EventQueueWithEmpty: - return queue.Empty() - default: - return q.Size() == 0 - } -} diff --git a/event/scheduler.go b/event/scheduler.go deleted file mode 100644 index 596d542..0000000 --- a/event/scheduler.go +++ /dev/null @@ -1,91 +0,0 @@ -package event - -import ( - "context" - "time" - - "github.com/benbjohnson/clock" -) - -// Scheduler is an interface for scheduling actions to run as soon as possible -// or at a specific time -type Scheduler interface { - // Now returns the time of the scheduler's clock - Clock() clock.Clock - - // EnqueueAction enqueues an action to run as soon as possible - EnqueueAction(context.Context, Action) - // ScheduleAction schedules an action to run at a specific time - ScheduleAction(context.Context, time.Time, Action) PlannedAction - // RemovePlannedAction removes an action from the scheduler planned actions - // (not from the queue), does nothing if the action is not in the planner - RemovePlannedAction(context.Context, PlannedAction) bool - - // RunOne runs one action from the scheduler's queue, returning true if an - // action was run, false if the queue was empty - RunOne(context.Context) bool -} - -// ScheduleActionIn schedules an action to run after a delay -func ScheduleActionIn(ctx context.Context, s Scheduler, d time.Duration, a Action) PlannedAction { - if d <= 0 { - s.EnqueueAction(ctx, a) - return nil - } else { - return s.ScheduleAction(ctx, s.Clock().Now().Add(d), a) - } -} - -// RunManyScheduler is a scheduler that can run multiple actions at once -type RunManyScheduler interface { - Scheduler - - // RunMany runs n actions on the scheduler, returning true if all actions - // were run, or false if there were less than n actions to run - RunMany(context.Context, int) bool -} - -// RunMany runs n actions on the scheduler, returning true if all actions were -// run, or false if there were less than n actions to run -func RunMany(ctx context.Context, s Scheduler, n int) bool { - switch s := s.(type) { - case RunManyScheduler: - return s.RunMany(ctx, n) - default: - for i := 0; i < n; i++ { - if !s.RunOne(ctx) { - return false - } - } - return true - } -} - -// RunAllScheduler is a scheduler that can run all actions in its queue -type RunAllScheduler interface { - Scheduler - - // RunAll runs all actions in the scheduler's queue - RunAll(context.Context) -} - -// RunAll runs all actions in the scheduler's queue and overdue actions from -// the planner -func RunAll(ctx context.Context, s Scheduler) { - switch s := s.(type) { - case RunAllScheduler: - s.RunAll(ctx) - default: - for s.RunOne(ctx) { - } - } -} - -// AwareScheduler is a scheduler that can return the time of the next scheduled -type AwareScheduler interface { - Scheduler - - // NextActionTime returns the time of the next action in the scheduler's - // queue or util.MaxTime if the queue is empty - NextActionTime(context.Context) time.Time -} diff --git a/event/simpleplanner.go b/event/simpleplanner.go deleted file mode 100644 index e63b8d8..0000000 --- a/event/simpleplanner.go +++ /dev/null @@ -1,114 +0,0 @@ -package event - -import ( - "context" - "sync" - "time" - - "github.com/benbjohnson/clock" -) - -type SimplePlanner struct { - Clock clock.Clock - - NextAction *simpleTimedAction - lock sync.Mutex -} - -var _ AwareActionPlanner = (*SimplePlanner)(nil) - -type simpleTimedAction struct { - action Action - time time.Time - next *simpleTimedAction -} - -var _ PlannedAction = (*simpleTimedAction)(nil) - -func (a *simpleTimedAction) Time() time.Time { - return a.time -} - -func (a *simpleTimedAction) Action() Action { - return a.action -} - -func NewSimplePlanner(clk clock.Clock) *SimplePlanner { - return &SimplePlanner{ - Clock: clk, - } -} - -func (p *SimplePlanner) ScheduleAction(ctx context.Context, t time.Time, a Action) PlannedAction { - p.lock.Lock() - defer p.lock.Unlock() - - if p.NextAction == nil { - p.NextAction = &simpleTimedAction{action: a, time: t} - return p.NextAction - } - - curr := p.NextAction - if t.Before(curr.time) { - p.NextAction = &simpleTimedAction{action: a, time: t, next: curr} - return p.NextAction - } - for curr.next != nil && t.After(curr.next.time) { - curr = curr.next - } - curr.next = &simpleTimedAction{action: a, time: t, next: curr.next} - return curr.next -} - -func (p *SimplePlanner) RemoveAction(ctx context.Context, pa PlannedAction) bool { - p.lock.Lock() - defer p.lock.Unlock() - - a, ok := pa.(*simpleTimedAction) - if !ok { - return false - } - - curr := p.NextAction - if curr == nil { - return false - } - - if curr == a { - p.NextAction = curr.next - return true - } - for curr.next != nil { - if curr.next == a { - curr.next = curr.next.next - return true - } - curr = curr.next - } - return false -} - -func (p *SimplePlanner) PopOverdueActions(ctx context.Context) []Action { - p.lock.Lock() - defer p.lock.Unlock() - - var overdue []Action - now := p.Clock.Now() - curr := p.NextAction - for curr != nil && (curr.time.Before(now) || curr.time == now) { - overdue = append(overdue, curr.action) - curr = curr.next - } - p.NextAction = curr - return overdue -} - -func (p *SimplePlanner) NextActionTime(context.Context) time.Time { - p.lock.Lock() - defer p.lock.Unlock() - - if p.NextAction == nil { - return MaxTime - } - return p.NextAction.time -} diff --git a/event/simpleplanner_test.go b/event/simpleplanner_test.go deleted file mode 100644 index 9449854..0000000 --- a/event/simpleplanner_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package event - -import ( - "context" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" -) - -func TestSimplePlanner(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - p := NewSimplePlanner(clk) - - nActions := 10 - actions := make([]Action, nActions) - for i := 0; i < nActions; i++ { - actions[i] = IntAction(i) - } - - a0 := p.ScheduleAction(ctx, clk.Now().Add(time.Millisecond), actions[0]) - require.Empty(t, p.PopOverdueActions(ctx)) - - clk.Add(time.Millisecond) - require.Equal(t, actions[:1], p.PopOverdueActions(ctx)) - require.Empty(t, p.PopOverdueActions(ctx)) - - p.ScheduleAction(ctx, clk.Now().Add(2*time.Minute), actions[1]) - p.ScheduleAction(ctx, clk.Now().Add(2*time.Second), actions[2]) - p.ScheduleAction(ctx, clk.Now().Add(time.Minute), actions[3]) - p.ScheduleAction(ctx, clk.Now().Add(time.Hour), actions[4]) - require.Empty(t, p.PopOverdueActions(ctx)) - - clk.Add(2 * time.Second) - require.Equal(t, actions[2:3], p.PopOverdueActions(ctx)) - - clk.Add(2 * time.Minute) - require.Equal(t, []Action{actions[3], actions[1]}, p.PopOverdueActions(ctx)) - - p.ScheduleAction(ctx, clk.Now().Add(time.Second), actions[5]) - clk.Add(time.Second) - require.Equal(t, actions[5:6], p.PopOverdueActions(ctx)) - - clk.Add(time.Hour) - require.Equal(t, actions[4:5], p.PopOverdueActions(ctx)) - - p.RemoveAction(ctx, a0) - - a6 := p.ScheduleAction(ctx, clk.Now().Add(time.Second), actions[6]) // 3 - p.ScheduleAction(ctx, clk.Now().Add(time.Microsecond), actions[7]) // 1 - a8 := p.ScheduleAction(ctx, clk.Now().Add(time.Hour), actions[8]) // 4 - a9 := p.ScheduleAction(ctx, clk.Now().Add(time.Millisecond), actions[9]) // 2 - - p.RemoveAction(ctx, a9) - p.RemoveAction(ctx, a0) - clk.Add(time.Second) - - p.RemoveAction(ctx, a6) - require.Equal(t, actions[7:8], p.PopOverdueActions(ctx)) - - p.RemoveAction(ctx, a8) - require.Empty(t, p.PopOverdueActions(ctx)) -} - -type otherPlannedAction int - -func (a otherPlannedAction) Action() Action { - return IntAction(int(a)) -} - -func (a otherPlannedAction) Time() time.Time { - return time.Time{} -} - -func TestSimplePlannedAction(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - p := NewSimplePlanner(clk) - - a0 := p.ScheduleAction(ctx, clk.Now().Add(time.Millisecond), IntAction(0)) - require.Equal(t, IntAction(0), a0.Action()) - require.Equal(t, clk.Now().Add(time.Millisecond), a0.Time()) - - a1 := otherPlannedAction(1) - p.RemoveAction(ctx, a1) - - clk.Add(time.Millisecond) - - overdueActions := p.PopOverdueActions(ctx) - require.Equal(t, 1, len(overdueActions)) - require.Equal(t, a0.Action(), overdueActions[0]) - require.Nil(t, p.PopOverdueActions(ctx)) -} - -func TestNextActionTime(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - p := NewSimplePlanner(clk) - - clk.Set(time.Unix(0, 0)) - actions := make([]Action, 3) - for i := 0; i < len(actions); i++ { - actions[i] = IntAction(i) - } - - ti := p.NextActionTime(ctx) - require.Equal(t, MaxTime, ti) - - t0 := clk.Now().Add(time.Second) - p.ScheduleAction(ctx, t0, actions[0]) - ti = p.NextActionTime(ctx) - require.Equal(t, t0, ti) - - t1 := clk.Now().Add(time.Hour) - p.ScheduleAction(ctx, t1, actions[1]) - ti = p.NextActionTime(ctx) - require.Equal(t, t0, ti) - - t2 := clk.Now().Add(time.Millisecond) - p.ScheduleAction(ctx, t2, actions[2]) - ti = p.NextActionTime(ctx) - require.Equal(t, t2, ti) - - require.Equal(t, 0, len(p.PopOverdueActions(ctx))) - - clk.Add(time.Millisecond) - ti = p.NextActionTime(ctx) - require.Equal(t, t2, ti) - - require.Equal(t, 1, len(p.PopOverdueActions(ctx))) - ti = p.NextActionTime(ctx) - require.Equal(t, t0, ti) - - clk.Add(time.Hour) - require.Equal(t, 2, len(p.PopOverdueActions(ctx))) - ti = p.NextActionTime(ctx) - require.Equal(t, MaxTime, ti) -} diff --git a/event/simplescheduler.go b/event/simplescheduler.go deleted file mode 100644 index a93b32a..0000000 --- a/event/simplescheduler.go +++ /dev/null @@ -1,90 +0,0 @@ -package event - -import ( - "context" - "time" - - "github.com/benbjohnson/clock" -) - -const DefaultChanqueueCapacity = 1024 - -// SimpleScheduler is a simple implementation of the Scheduler interface. It -// uses a simple planner and a channel-based queue. -type SimpleScheduler struct { - clk clock.Clock - - queue EventQueue - planner AwareActionPlanner -} - -var _ AwareScheduler = (*SimpleScheduler)(nil) - -// NewSimpleScheduler creates a new SimpleScheduler. -func NewSimpleScheduler(clk clock.Clock) *SimpleScheduler { - return &SimpleScheduler{ - clk: clk, - - queue: NewChanQueue(DefaultChanqueueCapacity), - planner: NewSimplePlanner(clk), - } -} - -// Now returns the scheduler's current time. -func (s *SimpleScheduler) Clock() clock.Clock { - return s.clk -} - -// EnqueueAction enqueues an action to be run as soon as possible. -func (s *SimpleScheduler) EnqueueAction(ctx context.Context, a Action) { - s.queue.Enqueue(ctx, a) -} - -// ScheduleAction schedules an action to run at a specific time. -func (s *SimpleScheduler) ScheduleAction(ctx context.Context, t time.Time, - a Action, -) PlannedAction { - if s.clk.Now().After(t) { - s.EnqueueAction(ctx, a) - return nil - } - return s.planner.ScheduleAction(ctx, t, a) -} - -// RemovePlannedAction removes an action from the scheduler planned actions -// (not from the queue), does nothing if the action is not in the planner -func (s *SimpleScheduler) RemovePlannedAction(ctx context.Context, a PlannedAction) bool { - return s.planner.RemoveAction(ctx, a) -} - -// moveOverdueActions moves all overdue actions from the planner to the queue. -func (s *SimpleScheduler) moveOverdueActions(ctx context.Context) { - overdue := s.planner.PopOverdueActions(ctx) - - EnqueueMany(ctx, s.queue, overdue) -} - -// RunOne runs one action from the scheduler's queue, returning true if an -// action was run, false if the queue was empty. -func (s *SimpleScheduler) RunOne(ctx context.Context) bool { - s.moveOverdueActions(ctx) - - if a := s.queue.Dequeue(ctx); a != nil { - a.Run(ctx) - return true - } - return false -} - -// NextActionTime returns the time of the next action to run, or the current -// time if there are actions to be run in the queue, or util.MaxTime if there -// are no scheduled to run. -func (s *SimpleScheduler) NextActionTime(ctx context.Context) time.Time { - s.moveOverdueActions(ctx) - nextScheduled := s.planner.NextActionTime(ctx) - - if !Empty(s.queue) { - return s.clk.Now() - } - return nextScheduled -} diff --git a/event/simplescheduler_test.go b/event/simplescheduler_test.go deleted file mode 100644 index 22f34a8..0000000 --- a/event/simplescheduler_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package event - -import ( - "context" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" -) - -func TestSimpleScheduler(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - sched := NewSimpleScheduler(clk) - - require.Equal(t, clk.Now(), sched.Clock().Now()) - - nActions := 10 - actions := make([]*FuncAction, nActions) - - for i := 0; i < nActions; i++ { - actions[i] = NewFuncAction(i) - } - - sched.EnqueueAction(ctx, actions[0]) - require.False(t, actions[0].Ran) - sched.RunOne(ctx) - require.True(t, actions[0].Ran) - - ScheduleActionIn(ctx, sched, time.Second, actions[1]) - require.False(t, actions[1].Ran) - sched.EnqueueAction(ctx, actions[2]) - clk.Add(2 * time.Second) - - sched.RunOne(ctx) - require.True(t, actions[2].Ran) - require.False(t, actions[1].Ran) - sched.RunOne(ctx) - require.True(t, actions[1].Ran) - sched.RunOne(ctx) - - ScheduleActionIn(ctx, sched, -1*time.Second, actions[3]) - require.False(t, actions[3].Ran) - sched.RunOne(ctx) - require.True(t, actions[3].Ran) - - sched.ScheduleAction(ctx, clk.Now().Add(-1*time.Nanosecond), actions[4]) - require.False(t, actions[4].Ran) - sched.RunOne(ctx) - require.True(t, actions[4].Ran) - - sched.ScheduleAction(ctx, clk.Now().Add(time.Second), actions[5]) - sched.RunOne(ctx) - require.False(t, actions[5].Ran) - clk.Add(time.Second) - require.Equal(t, clk.Now(), sched.NextActionTime(ctx)) - sched.RunOne(ctx) - require.True(t, actions[5].Ran) - - t6 := clk.Now().Add(time.Second) - a6 := sched.ScheduleAction(ctx, t6, actions[6]) - require.Equal(t, t6, sched.NextActionTime(ctx)) - sched.RemovePlannedAction(ctx, a6) - clk.Add(time.Second) - sched.RunOne(ctx) - require.False(t, actions[6].Ran) - // empty queue - require.Equal(t, MaxTime, sched.NextActionTime(ctx)) -} diff --git a/event/testaction.go b/event/testaction.go deleted file mode 100644 index 4d15770..0000000 --- a/event/testaction.go +++ /dev/null @@ -1,33 +0,0 @@ -package event - -import ( - "context" -) - -// IntAction is an action that does nothing but is used to test the scheduler. -// An IntAction is equal to another IntAction if they have the same integer -type IntAction int - -var _ Action = (*IntAction)(nil) - -// Run does nothing -func (a IntAction) Run(context.Context) {} - -// FuncAction is an action that does nothing but tracks whether it was "run" -// yet. It is used to test the scheduler. -type FuncAction struct { - Ran bool - Int int -} - -var _ Action = (*FuncAction)(nil) - -// NewFuncAction returns a new FuncAction -func NewFuncAction(i int) *FuncAction { - return &FuncAction{Int: i} -} - -// Run sets Ran to true -func (a *FuncAction) Run(context.Context) { - a.Ran = true -} diff --git a/event/testaction_test.go b/event/testaction_test.go deleted file mode 100644 index 65ec388..0000000 --- a/event/testaction_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package event - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestIntAction(t *testing.T) { - a := IntAction(0) - a.Run(context.Background()) - b := IntAction(1) - - require.NotEqual(t, a, b) -} - -func TestFuncAction(t *testing.T) { - a := NewFuncAction(0) - b := NewFuncAction(1) - require.NotEqual(t, a, b) - - a.Run(context.Background()) - require.True(t, a.Ran) - require.False(t, b.Ran) -} diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 5878510..0000000 --- a/examples/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Examples - -This folder contains Kademlia examples built with this repository. - -## Featured examples - -- ### [`connect`](./connect/) - IPFS DHT client example, where a Kademlia node configured with a `libp2pendpoint` connects to a bootstrapper node from the IPFS DHT and performs a `FIND_PEER` request in the live IPFS network. -- ### [`fullsim`](./fullsim/) - Simulation example where 4 simple nodes are created, and a node performs a multi hop lookup request. All nodes run in server mode since they have to answer each other's queries. \ No newline at end of file diff --git a/examples/connect/README.md b/examples/connect/README.md deleted file mode 100644 index 65b7abb..0000000 --- a/examples/connect/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Connecting to the IPFS DHT - -The goal of this test is to connect to the IPFS DHT. - diff --git a/examples/connect/findpeer.go b/examples/connect/findpeer.go deleted file mode 100644 index c7db36d..0000000 --- a/examples/connect/findpeer.go +++ /dev/null @@ -1,144 +0,0 @@ -package main - -import ( - "context" - "fmt" - "time" - - "github.com/benbjohnson/clock" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - - "github.com/plprobelab/go-kademlia/event" - tutil "github.com/plprobelab/go-kademlia/examples/util" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/libp2p" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/query/simplequery" - "github.com/plprobelab/go-kademlia/routing/simplert" - "github.com/plprobelab/go-kademlia/util" -) - -var protocolID address.ProtocolID = "/ipfs/kad/1.0.0" // IPFS DHT network protocol ID - -func FindPeer(ctx context.Context) { - ctx, span := util.StartSpan(ctx, "FindPeer Test") - defer span.End() - - // this example is using real time - clk := clock.New() - - // create a libp2p host - h, err := tutil.Libp2pHost(ctx, "8888") - if err != nil { - panic(err) - } - - pid := libp2p.NewPeerID(h.ID()) - - // create a simple routing table, with bucket size 20 - rt := simplert.New[key.Key256, kad.NodeID[key.Key256]](pid, 20) - // create a scheduler using real time - sched := event.NewSimpleScheduler(clk) - // create a message endpoint is used to communicate with other peers - msgEndpoint := libp2p.NewLibp2pEndpoint(ctx, h, sched) - - // friend is the first peer we know in the IPFS DHT network (bootstrap node) - friend, err := peer.Decode("12D3KooWGjgvfDkpuVAoNhd7PRRvMTEG4ZgzHBFURqDe1mqEzAMS") - if err != nil { - panic(err) - } - friendID := libp2p.NewPeerID(friend) - - // multiaddress of friend - a, err := multiaddr.NewMultiaddr("/ip4/45.32.75.236/udp/4001/quic") - if err != nil { - panic(err) - } - // connect to friend - friendAddr := peer.AddrInfo{ID: friend, Addrs: []multiaddr.Multiaddr{a}} - if err := h.Connect(ctx, friendAddr); err != nil { - panic(err) - } - fmt.Println("connected to friend") - - // target is the peer we want to find QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb - target, err := peer.Decode("QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb") - if err != nil { - panic(err) - } - targetID := libp2p.NewPeerID(target) - - // create a find peer request message - req := libp2p.FindPeerRequest(targetID) - // add friend to routing table - success := rt.AddNode(friendID) - if !success { - panic("failed to add friend to rt") - } - - // endCond is used to terminate the simulation once the query is done - endCond := false - handleResultsFn := func(ctx context.Context, id kad.NodeID[key.Key256], - resp kad.Response[key.Key256, multiaddr.Multiaddr], - ) (bool, []kad.NodeID[key.Key256]) { - // parse response to ipfs dht message - msg, ok := resp.(*libp2p.Message) - if !ok { - fmt.Println("invalid response!") - return false, nil - } - var targetAddrs *libp2p.AddrInfo - peers := make([]kad.NodeID[key.Key256], 0, len(msg.CloserPeers)) - for _, p := range msg.CloserPeers { - addrInfo, err := libp2p.PBPeerToPeerInfo(p) - if err != nil { - fmt.Println("invalid peer info format") - continue - } - peers = append(peers, addrInfo.PeerID()) - if addrInfo.PeerID().ID == target { - endCond = true - targetAddrs = addrInfo - } - } - fmt.Println("---\nResponse from", id, "with", peers) - if endCond { - fmt.Println("\n - target found!", target, targetAddrs.Addrs) - } - // return peers and not msg.CloserPeers because we want to return the - // PeerIDs and not AddrInfos. The returned NodeID is used to update the - // query. The AddrInfo is only useful for the message endpoint. - return endCond, peers - } - - // create the query, the IPFS DHT protocol ID, the IPFS DHT request message, - // a concurrency parameter of 1, a timeout of 5 seconds, the libp2p message - // endpoint, the node's routing table and scheduler, and the response - // handler function. - // The query will be executed only once actions are run on the scheduler. - // For now, it is only scheduled to be run. - queryOpts := []simplequery.Option[key.Key256, multiaddr.Multiaddr]{ - simplequery.WithProtocolID[key.Key256, multiaddr.Multiaddr](protocolID), - simplequery.WithConcurrency[key.Key256, multiaddr.Multiaddr](1), - simplequery.WithRequestTimeout[key.Key256, multiaddr.Multiaddr](2 * time.Second), - simplequery.WithHandleResultsFunc[key.Key256, multiaddr.Multiaddr](handleResultsFn), - simplequery.WithRoutingTable[key.Key256, multiaddr.Multiaddr](rt), - simplequery.WithEndpoint[key.Key256, multiaddr.Multiaddr](msgEndpoint), - simplequery.WithScheduler[key.Key256, multiaddr.Multiaddr](sched), - } - _, err = simplequery.NewSimpleQuery[key.Key256, multiaddr.Multiaddr](ctx, pid.NodeID(), req, queryOpts...) - if err != nil { - panic(err) - } - - span.AddEvent("start request execution") - - // run the actions from the scheduler until the query is done - for i := 0; i < 1000 && !endCond; i++ { - for sched.RunOne(ctx) { - } - time.Sleep(10 * time.Millisecond) - } -} diff --git a/examples/connect/main.go b/examples/connect/main.go deleted file mode 100644 index 40f34b8..0000000 --- a/examples/connect/main.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "context" - "log" - "time" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/jaeger" - "go.opentelemetry.io/otel/sdk/resource" - "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.17.0" -) - -func main() { - tp, err := tracerProvider("http://localhost:14268/api/traces") - if err != nil { - log.Fatal(err) - } - - // Register our TracerProvider as the global so any imported - // instrumentation in the future will default to using it. - otel.SetTracerProvider(tp) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Cleanly shutdown and flush telemetry when the application exits. - defer func(ctx context.Context) { - // Do not make the application hang when it is shutdown. - ctx, cancel = context.WithTimeout(ctx, time.Second*5) - defer cancel() - if err := tp.Shutdown(ctx); err != nil { - log.Fatal(err) - } - }(ctx) - - FindPeer(ctx) -} - -// tracerProvider returns an OpenTelemetry TracerProvider configured to use -// the Jaeger exporter that will send spans to the provided url. The returned -// TracerProvider will also use a Resource configured with all the information -// about the application. -func tracerProvider(url string) (*trace.TracerProvider, error) { - // Create the Jaeger exporter - exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url))) - if err != nil { - return nil, err - } - tp := trace.NewTracerProvider( - // Always be sure to batch in production. - trace.WithBatcher(exp), - // Record information about this application in a Resource. - trace.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceName("Kademlia-Test"), - semconv.ServiceVersion("v0.1.0"), - attribute.String("environment", "demo"), - )), - ) - return tp, nil -} diff --git a/examples/dispatchquery/main.go b/examples/dispatchquery/main.go deleted file mode 100644 index 9907693..0000000 --- a/examples/dispatchquery/main.go +++ /dev/null @@ -1,199 +0,0 @@ -package main - -import ( - "context" - "log" - "time" - - "github.com/benbjohnson/clock" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - "github.com/multiformats/go-multibase" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/jaeger" - "go.opentelemetry.io/otel/sdk/resource" - "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/libp2p" - sq "github.com/plprobelab/go-kademlia/query/simplequery" - "github.com/plprobelab/go-kademlia/routing/simplert" - "github.com/plprobelab/go-kademlia/server/basicserver" - "github.com/plprobelab/go-kademlia/sim" - "github.com/plprobelab/go-kademlia/util" -) - -const ( - peerstoreTTL = 10 * time.Minute - protoID = "/ipfs/kad/1.0.0" -) - -var targetBytesID = "mACQIARIgp9PBu+JuU8aicuW8xT+Oa08OntMyqdLbfQtOplAHlME" - -func queryTest(ctx context.Context) { - ctx, span := util.StartSpan(ctx, "queryTest") - defer span.End() - - clk := clock.NewMock() - - router := sim.NewRouter[key.Key256, multiaddr.Multiaddr]() - - // create peer A - pidA, err := peer.Decode("12BooooALPHA") - if err != nil { - panic(err) - } - selfA := &libp2p.PeerID{ID: pidA} // peer.ID is necessary for ipfskadv1 message format - addrA := multiaddr.StringCast("/ip4/1.1.1.1/tcp/4001/") - var naddrA kad.NodeInfo[key.Key256, multiaddr.Multiaddr] = libp2p.NewAddrInfo(peer.AddrInfo{ - ID: selfA.ID, - Addrs: []multiaddr.Multiaddr{addrA}, - }) - rtA := simplert.New[key.Key256, kad.NodeID[key.Key256]](selfA, 2) - schedA := event.NewSimpleScheduler(clk) - endpointA := sim.NewEndpoint(selfA.NodeID(), schedA, router) - servA := basicserver.NewBasicServer[multiaddr.Multiaddr](rtA, endpointA) - err = endpointA.AddRequestHandler(protoID, nil, servA.HandleRequest) - if err != nil { - panic(err) - } - - // create peer B - pidB, err := peer.Decode("12BoooooBETA") - if err != nil { - panic(err) - } - selfB := &libp2p.PeerID{ID: pidB} - addrB := multiaddr.StringCast("/ip4/2.2.2.2/tcp/4001/") - var naddrB kad.NodeInfo[key.Key256, multiaddr.Multiaddr] = libp2p.NewAddrInfo(peer.AddrInfo{ - ID: selfB.ID, - Addrs: []multiaddr.Multiaddr{addrB}, - }) - rtB := simplert.New[key.Key256, kad.NodeID[key.Key256]](selfB, 2) - schedB := event.NewSimpleScheduler(clk) - endpointB := sim.NewEndpoint(selfB.NodeID(), schedB, router) - servB := basicserver.NewBasicServer[multiaddr.Multiaddr](rtB, endpointB) - err = endpointB.AddRequestHandler(protoID, nil, servB.HandleRequest) - if err != nil { - panic(err) - } - - // create peer C - pidC, err := peer.Decode("12BooooGAMMA") - if err != nil { - panic(err) - } - selfC := &libp2p.PeerID{ID: pidC} - addrC := multiaddr.StringCast("/ip4/3.3.3.3/tcp/4001/") - var naddrC kad.NodeInfo[key.Key256, multiaddr.Multiaddr] = libp2p.NewAddrInfo(peer.AddrInfo{ - ID: selfC.ID, - Addrs: []multiaddr.Multiaddr{addrC}, - }) - rtC := simplert.New[key.Key256, kad.NodeID[key.Key256]](selfC, 2) - schedC := event.NewSimpleScheduler(clk) - endpointC := sim.NewEndpoint(selfC.NodeID(), schedC, router) - servC := basicserver.NewBasicServer[multiaddr.Multiaddr](rtC, endpointC) - err = endpointC.AddRequestHandler(protoID, nil, servC.HandleRequest) - if err != nil { - panic(err) - } - - // connect peer A and B - endpointA.MaybeAddToPeerstore(ctx, naddrB, peerstoreTTL) - rtA.AddNode(selfB) - endpointB.MaybeAddToPeerstore(ctx, naddrA, peerstoreTTL) - rtB.AddNode(selfA) - - // connect peer B and C - endpointB.MaybeAddToPeerstore(ctx, naddrC, peerstoreTTL) - rtB.AddNode(selfC) - endpointC.MaybeAddToPeerstore(ctx, naddrB, peerstoreTTL) - rtC.AddNode(selfB) - - // create find peer request - _, bin, _ := multibase.Decode(targetBytesID) - target := libp2p.NewPeerID(peer.ID(bin)) - req := libp2p.FindPeerRequest(target) - - // dummy parameters - handleResp := func(ctx context.Context, _ kad.NodeID[key.Key256], - resp kad.Response[key.Key256, multiaddr.Multiaddr], - ) (bool, []kad.NodeID[key.Key256]) { - peerids := make([]kad.NodeID[key.Key256], len(resp.CloserNodes())) - for i, p := range resp.CloserNodes() { - peerids[i] = p.(*libp2p.AddrInfo).PeerID() - } - return false, peerids - } - - queryOpts := []sq.Option[key.Key256, multiaddr.Multiaddr]{ - sq.WithProtocolID[key.Key256, multiaddr.Multiaddr](protoID), - sq.WithConcurrency[key.Key256, multiaddr.Multiaddr](1), - sq.WithRequestTimeout[key.Key256, multiaddr.Multiaddr](5 * time.Second), - sq.WithHandleResultsFunc[key.Key256, multiaddr.Multiaddr](handleResp), - sq.WithRoutingTable[key.Key256, multiaddr.Multiaddr](rtA), - sq.WithEndpoint[key.Key256, multiaddr.Multiaddr](endpointA), - sq.WithScheduler[key.Key256, multiaddr.Multiaddr](schedA), - } - sq.NewSimpleQuery[key.Key256, multiaddr.Multiaddr](ctx, selfA.NodeID(), req, queryOpts...) - - // create simulator - s := sim.NewLiteSimulator(clk) - sim.AddSchedulers(s, schedA, schedB, schedC) - // run simulation - s.Run(ctx) -} - -// tracerProvider returns an OpenTelemetry TracerProvider configured to use -// the Jaeger exporter that will send spans to the provided url. The returned -// TracerProvider will also use a Resource configured with all the information -// about the application. -func tracerProvider(url string) (*trace.TracerProvider, error) { - // Create the Jaeger exporter - exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url))) - if err != nil { - return nil, err - } - tp := trace.NewTracerProvider( - // Always be sure to batch in production. - trace.WithBatcher(exp), - // Record information about this application in a Resource. - trace.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceName("Kademlia-Test"), - semconv.ServiceVersion("v0.1.0"), - attribute.String("environment", "demo"), - )), - ) - return tp, nil -} - -func main() { - tp, err := tracerProvider("http://localhost:14268/api/traces") - if err != nil { - log.Fatal(err) - } - - // Register our TracerProvider as the global so any imported - // instrumentation in the future will default to using it. - otel.SetTracerProvider(tp) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Cleanly shutdown and flush telemetry when the application exits. - defer func(ctx context.Context) { - // Do not make the application hang when it is shutdown. - ctx, cancel = context.WithTimeout(ctx, time.Second*5) - defer cancel() - if err := tp.Shutdown(ctx); err != nil { - log.Fatal(err) - } - }(ctx) - - queryTest(ctx) -} diff --git a/examples/fullsim/findnode.go b/examples/fullsim/findnode.go deleted file mode 100644 index e207077..0000000 --- a/examples/fullsim/findnode.go +++ /dev/null @@ -1,143 +0,0 @@ -package main - -import ( - "context" - "fmt" - "net" - "time" - - "github.com/benbjohnson/clock" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" - sq "github.com/plprobelab/go-kademlia/query/simplequery" - "github.com/plprobelab/go-kademlia/routing/simplert" - "github.com/plprobelab/go-kademlia/server" - "github.com/plprobelab/go-kademlia/sim" - "github.com/plprobelab/go-kademlia/util" -) - -const ( - peerstoreTTL = 10 * time.Minute // duration for which a peer is kept in the peerstore - protoID = address.ProtocolID("/test/1.0.0") // protocol ID for the test -) - -// connectNodes adds nodes to each other's peerstores and routing tables -func connectNodes(ctx context.Context, n0, n1 kad.NodeInfo[key.Key8, net.IP], ep0, ep1 endpoint.Endpoint[key.Key8, net.IP], - rt0, rt1 kad.RoutingTable[key.Key8, kad.NodeID[key.Key8]], -) { - // add n1 to n0's peerstore and routing table - ep0.MaybeAddToPeerstore(ctx, n1, peerstoreTTL) - rt0.AddNode(n1.ID()) - // add n0 to n1's peerstore and routing table - ep1.MaybeAddToPeerstore(ctx, n0, peerstoreTTL) - rt1.AddNode(n0.ID()) -} - -func findNode(ctx context.Context) { - ctx, span := util.StartSpan(ctx, "findNode test") - defer span.End() - - // create mock clock to control time - clk := clock.NewMock() - // create a fake router to virtually connect nodes - router := sim.NewRouter[key.Key8, net.IP]() - - // create node identifiers - nodeCount := 4 - nodes := make([]*kadtest.Info[key.Key8, net.IP], nodeCount) - nodes[0] = kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0)), nil) - nodes[1] = kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0x01)), nil) - nodes[2] = kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0x02)), nil) - nodes[3] = kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0x03)), nil) - - // Kademlia trie: - // ^ - // / \ - // ^ ^ - // A B C D - - rts := make([]*simplert.SimpleRT[key.Key8, kad.NodeID[key.Key8]], len(nodes)) - eps := make([]*sim.Endpoint[key.Key8, net.IP], len(nodes)) - schedulers := make([]event.AwareScheduler, len(nodes)) - servers := make([]server.Server[key.Key8], len(nodes)) - - for i := 0; i < len(nodes); i++ { - // create a routing table, with bucket size 2 - rts[i] = simplert.New[key.Key8, kad.NodeID[key.Key8]](nodes[i].ID(), 2) - // create a scheduler based on the mock clock - schedulers[i] = event.NewSimpleScheduler(clk) - // create a fake endpoint for the node, communicating through the router - eps[i] = sim.NewEndpoint[key.Key8, net.IP](nodes[i].ID(), schedulers[i], router) - // create a server instance for the node - servers[i] = sim.NewServer[key.Key8, net.IP](rts[i], eps[i], sim.DefaultServerConfig()) - // add the server request handler for protoID to the endpoint - err := eps[i].AddRequestHandler(protoID, nil, servers[i].HandleRequest) - if err != nil { - panic(err) - } - } - - // A connects to B - connectNodes(ctx, nodes[0], nodes[1], eps[0], eps[1], rts[0], rts[1]) - - // B connects to C - connectNodes(ctx, nodes[1], nodes[2], eps[1], eps[2], rts[1], rts[2]) - - // C connects to D - connectNodes(ctx, nodes[2], nodes[3], eps[2], eps[3], rts[2], rts[3]) - - // A (ids[0]) is looking for D (ids[3]) - // A will first ask B, B will reply with C's address (and A's address) - // A will then ask C, C will reply with D's address (and B's address) - req := sim.NewRequest[key.Key8, net.IP](nodes[3].ID().Key()) - - // handleResFn is called when a response is received during the query process - handleResFn := func(_ context.Context, id kad.NodeID[key.Key8], - msg kad.Response[key.Key8, net.IP], - ) (bool, []kad.NodeID[key.Key8]) { - resp := msg.(*sim.Message[key.Key8, net.IP]) - fmt.Println("got a response from", id, "with", resp.CloserNodes()) - - newIds := make([]kad.NodeID[key.Key8], len(resp.CloserNodes())) - for i, peer := range resp.CloserNodes() { - if kad.Equal(peer.ID(), nodes[3].ID()) { - // the response contains the address of D (ids[3]) - fmt.Println("success") - // returning true will stop the query process - return true, nil - } - newIds[i] = peer.ID() - } - // returning false will continue the query process - return false, newIds - } - - // create a query on A (using A's scheduler, endpoint and routing table), - // D's Kademlia Key as target, the defined protocol ID, using req as the - // request message, an empty Message (resp) as the response message, a - // concurrency of 1, a timeout of 1 second, and handleResFn as the response - // handler. The query doesn't run yet, it is added to A's event queue - // through A's scheduler. - queryOpts := []sq.Option[key.Key8, net.IP]{ - sq.WithProtocolID[key.Key8, net.IP](protoID), - sq.WithConcurrency[key.Key8, net.IP](1), - sq.WithRequestTimeout[key.Key8, net.IP](time.Second), - sq.WithHandleResultsFunc(handleResFn), - sq.WithRoutingTable[key.Key8, net.IP](rts[0]), - sq.WithEndpoint[key.Key8, net.IP](eps[0]), - sq.WithScheduler[key.Key8, net.IP](schedulers[0]), - } - sq.NewSimpleQuery[key.Key8, net.IP](ctx, nodes[0].ID(), req, queryOpts...) - - // create a simulator, simulating [A, B, C, D]'s simulators - s := sim.NewLiteSimulator(clk) - sim.AddSchedulers(s, schedulers...) - - // run the simulation until all events are processed - s.Run(ctx) -} diff --git a/examples/fullsim/main.go b/examples/fullsim/main.go deleted file mode 100644 index ec9bb88..0000000 --- a/examples/fullsim/main.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "context" - "log" - "time" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/jaeger" - "go.opentelemetry.io/otel/sdk/resource" - "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.17.0" -) - -func main() { - tp, err := tracerProvider("http://localhost:14268/api/traces") - if err != nil { - log.Fatal(err) - } - - // Register our TracerProvider as the global so any imported - // instrumentation in the future will default to using it. - otel.SetTracerProvider(tp) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Cleanly shutdown and flush telemetry when the application exits. - defer func(ctx context.Context) { - // Do not make the application hang when it is shutdown. - ctx, cancel = context.WithTimeout(ctx, time.Second*5) - defer cancel() - if err := tp.Shutdown(ctx); err != nil { - log.Fatal(err) - } - }(ctx) - - findNode(ctx) -} - -// tracerProvider returns an OpenTelemetry TracerProvider configured to use -// the Jaeger exporter that will send spans to the provided url. The returned -// TracerProvider will also use a Resource configured with all the information -// about the application. -func tracerProvider(url string) (*trace.TracerProvider, error) { - // Create the Jaeger exporter - exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url))) - if err != nil { - return nil, err - } - tp := trace.NewTracerProvider( - // Always be sure to batch in production. - trace.WithBatcher(exp), - // Record information about this application in a Resource. - trace.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceName("Kademlia-Test"), - semconv.ServiceVersion("v0.1.0"), - attribute.String("environment", "demo"), - )), - ) - return tp, nil -} diff --git a/examples/util/util.go b/examples/util/util.go deleted file mode 100644 index 75cc3f9..0000000 --- a/examples/util/util.go +++ /dev/null @@ -1,96 +0,0 @@ -package util - -import ( - "context" - "time" - - // varint is here - - mc "github.com/multiformats/go-multicodec" - mh "github.com/multiformats/go-multihash" - - "github.com/ipfs/go-cid" - "github.com/libp2p/go-libp2p/p2p/net/connmgr" - - "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/p2p/security/noise" - libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls" -) - -func Libp2pHost(ctx context.Context, port string) (host.Host, error) { - // Set your own keypair - priv, _, err := crypto.GenerateKeyPair( - crypto.Ed25519, // Select your key type. Ed25519 are nice short - -1, // Select key length when possible (i.e. RSA). - ) - if err != nil { - panic(err) - } - - connmgr, err := connmgr.NewConnManager( - 100, // Lowwater - 400, // HighWater, - connmgr.WithGracePeriod(time.Minute), - ) - if err != nil { - panic(err) - } - h, err := libp2p.New( - // Use the keypair we generated - libp2p.Identity(priv), - // Multiple listen addresses - libp2p.ListenAddrStrings( - "/ip4/0.0.0.0/tcp/"+port, // regular tcp connections - "/ip4/0.0.0.0/udp/"+port+"/quic", // a UDP endpoint for the QUIC transport - ), - // support TLS connections - libp2p.Security(libp2ptls.ID, libp2ptls.New), - // support noise connections - libp2p.Security(noise.ID, noise.New), - // support any other default transports (TCP) - libp2p.DefaultTransports, - // Let's prevent our peer from having too many - // connections by attaching a connection manager. - libp2p.ConnectionManager(connmgr), - // Attempt to open ports using uPNP for NATed hosts. - libp2p.NATPortMap(), - /* - // Let this host use the DHT to find other hosts - libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) { - idht = dht.NewDHT(ctx, h) - return idht, err - }), - */ - // If you want to help other peers to figure out if they are behind - // NATs, you can launch the server-side of AutoNAT too (AutoRelay - // already runs the client) - // - // This service is highly rate-limited and should not cause any - // performance issues. - libp2p.EnableNATService(), - ) - if err != nil { - panic(err) - } - return h, nil -} - -func GenCid() cid.Cid { - // Create a cid manually by specifying the 'prefix' parameters - pref := cid.Prefix{ - Version: 1, - Codec: uint64(mc.Raw), - MhType: mh.SHA2_256, - MhLength: -1, // default length - } - - // And then feed it some data - c, err := pref.Sum([]byte("Hello World!")) - if err != nil { - panic(err) - } - - return c -} diff --git a/go.mod b/go.mod index 616458b..4fe0840 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/plprobelab/go-kademlia +module github.com/libp2p/go-libdht go 1.21 @@ -11,6 +11,7 @@ require ( github.com/multiformats/go-multibase v0.2.0 github.com/multiformats/go-multicodec v0.9.0 github.com/multiformats/go-multihash v0.2.3 + github.com/plprobelab/go-kademlia v0.0.0-20230914085641-54e0b94d02bd github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/exporters/jaeger v1.16.0 @@ -48,7 +49,6 @@ require ( github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/koron/go-ssdp v0.0.4 // indirect - github.com/kr/text v0.2.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect @@ -90,7 +90,6 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/fx v1.20.0 // indirect - go.uber.org/goleak v1.2.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.11.0 // indirect diff --git a/go.sum b/go.sum index f6c2e5e..ad29416 100644 --- a/go.sum +++ b/go.sum @@ -31,7 +31,6 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -226,6 +225,8 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhM github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/plprobelab/go-kademlia v0.0.0-20230914085641-54e0b94d02bd h1:tNN03Hlj90guiA8JRACUb43hwpvQwdcqz8vlVmKhlzI= +github.com/plprobelab/go-kademlia v0.0.0-20230914085641-54e0b94d02bd/go.mod h1:9mz9/8plJj9HWiQmB6JkBNHY30AXzy9LrJ++sCvWqFQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= diff --git a/kad/doc.go b/kad/doc.go deleted file mode 100644 index 7e06473..0000000 --- a/kad/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package kad provides interfaces defining core Kademlia types -package kad diff --git a/kad/kad.go b/kad/kad.go deleted file mode 100644 index 815df3a..0000000 --- a/kad/kad.go +++ /dev/null @@ -1,152 +0,0 @@ -package kad - -import ( - "context" -) - -// Key is the interface all Kademlia key types support. -// -// A Kademlia key is defined as a bit string of arbitrary size. In practice, different Kademlia implementations use -// different key sizes. For instance, the Kademlia paper (https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) -// defines keys as 160-bits long and IPFS uses 256-bit keys. -// -// Keys are usually generated using cryptographic hash functions, however the specifics of key generation -// do not matter for key operations. -// -// A Key is not necessarily used to identify a node in the network but a derived -// representation. Implementations may choose to hash a logical node identifier -// to derive a Kademlia Key. Therefore, there also exists the concept of a NodeID -// which just defines a method to return the associated Kademlia Key. -type Key[K any] interface { - // BitLen returns the length of the key in bits. - BitLen() int - - // Bit returns the value of the i'th bit of the key from most significant to least. It is equivalent to (key>>(bitlen-i-1))&1. - // Bit will panic if i is out of the range [0,BitLen()-1]. - Bit(i int) uint - - // Xor returns the result of the eXclusive OR operation between the key and another key of the same type. - Xor(other K) K - - // CommonPrefixLength returns the number of leading bits the key shares with another key of the same type. - // The CommonPrefixLength of a key with itself is equal to BitLen. - CommonPrefixLength(other K) int - - // Compare compares the numeric value of the key with another key of the same type. - // It returns -1 if the key is numerically less than other, +1 if it is greater - // and 0 if both keys are equal. - Compare(other K) int -} - -// RoutingTable is the interface all Kademlia Routing Tables types support. -type RoutingTable[K Key[K], N NodeID[K]] interface { - // AddNode tries to add a peer to the routing table. It returns true if - // the node was added and false if it wasn't added, e.g., because it - // was already part of the routing table. - // - // Because NodeID[K]'s are often preimages to Kademlia keys K - // there's no way to derive a NodeID[K] from just K. Therefore, to be - // able to return NodeID[K]'s from the `NearestNodes` method, this - // `AddNode` method signature takes a NodeID[K] instead of only K. - // - // Nodes added to the routing table are grouped into buckets based on their - // XOR distance to the local node's identifier. The details of the XOR - // arithmetics are defined on K. - AddNode(N) bool - - // RemoveKey tries to remove a node identified by its Kademlia key from the - // routing table. - // - // It returns true if the key existed in the routing table and was removed. - // It returns false if the key didn't exist in the routing table and - // therefore, was not removed. - RemoveKey(K) bool - - // NearestNodes returns the given number of closest nodes to a given - // Kademlia key that are currently present in the routing table. - // The returned list of nodes will be ordered from closest to furthest and - // contain at maximum the given number of entries, but also possibly less - // if the number exceeds the number of nodes in the routing table. - NearestNodes(K, int) []N - - // GetNode returns the node identified by the supplied Kademlia key or a zero - // value if the node is not present in the routing table. The boolean second - // return value indicates whether the node was found in the table. - GetNode(K) (N, bool) -} - -// NodeID is a generic node identifier and not equal to a Kademlia key. Some -// implementations use NodeID's as preimages for Kademlia keys. Kademlia keys -// are used for calculating distances between nodes while NodeID's are the -// original logical identifier of a node. -// -// The NodeID interface only defines a method that returns the Kademlia key -// for the given NodeID. E.g., the operation to go from a NodeID to a Kademlia key -// can be as simple as hashing the NodeID. -// -// Implementations may choose to equate NodeID's and Kademlia keys. -type NodeID[K Key[K]] interface { - // Key returns the Kademlia key of the given NodeID. E.g., NodeID's can be - // preimages to Kademlia keys, in which case, Key() could return the SHA256 - // of NodeID. - Key() K - - // String returns a string reprensentation for this NodeID. - // TODO: Try to get rid of this as it's also used for map keys which is not great. - String() string -} - -// NodeInfo is a container type that combines node identification information -// and network addresses at which the node is reachable. -type NodeInfo[K Key[K], A Address[A]] interface { - // ID returns the node identifier. - ID() NodeID[K] - - // Addresses returns the network addresses associated with the given node. - Addresses() []A -} - -// Address is an interface that any type must implement that can be used -// to address a node in the DHT network. This can be an IP/Port combination -// or in the case of libp2p a Multiaddress. -type Address[T any] interface { - // Equal re - Equal(T) bool -} - -// Equal checks the equality of two NodeIDs. -// TODO: move somewhere else. -func Equal[K Key[K]](this, that NodeID[K]) bool { - return this.Key().Compare(that.Key()) == 0 -} - -type Message interface{} - -type Request[K Key[K], A Address[A]] interface { - Message - - // Target returns the target key and true, or false if no target key has been specfied. - Target() K - - // EmptyResponse returns an empty response struct for this request message - // TODO: this is a weird patter, let's try to remove this. - EmptyResponse() Response[K, A] -} - -type Response[K Key[K], A Address[A]] interface { - Message - - CloserNodes() []NodeInfo[K, A] -} - -type RoutingProtocol[K Key[K], N NodeID[K], A Address[A]] interface { - FindNode(ctx context.Context, to N, target K) (NodeInfo[K, A], []N, error) - Ping(ctx context.Context, to N) error -} - -type RecordProtocol[K Key[K], N NodeID[K]] interface { - Get(ctx context.Context, to N, target K) ([]Record, []N, error) - Put(ctx context.Context, to N, record Record) error -} - -type Record any diff --git a/internal/kadtest/bench.go b/kad/kadtest/bench.go similarity index 100% rename from internal/kadtest/bench.go rename to kad/kadtest/bench.go diff --git a/internal/kadtest/bench_pre120.go b/kad/kadtest/bench_pre120.go similarity index 100% rename from internal/kadtest/bench_pre120.go rename to kad/kadtest/bench_pre120.go diff --git a/internal/kadtest/context.go b/kad/kadtest/context.go similarity index 100% rename from internal/kadtest/context.go rename to kad/kadtest/context.go diff --git a/internal/kadtest/ids.go b/kad/kadtest/ids.go similarity index 55% rename from internal/kadtest/ids.go rename to kad/kadtest/ids.go index 6ce09dd..870a698 100644 --- a/internal/kadtest/ids.go +++ b/kad/kadtest/ids.go @@ -2,10 +2,9 @@ package kadtest import ( "crypto/sha256" - "net" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" + "github.com/libp2p/go-libdht/kad" + "github.com/libp2p/go-libdht/key" ) // ID is a concrete implementation of the NodeID interface. @@ -42,19 +41,19 @@ func (i ID[K]) String() string { type StringID string -var _ kad.NodeID[key.Key256] = (*StringID)(nil) +var _ kad.NodeID[kad.Key256] = (*StringID)(nil) func NewStringID(s string) *StringID { return (*StringID)(&s) } -func (s StringID) Key() key.Key256 { +func (s StringID) Key() kad.Key256 { h := sha256.New() h.Write([]byte(s)) - return key.NewKey256(h.Sum(nil)) + return kad.NewKey256(h.Sum(nil)) } -func (s StringID) NodeID() kad.NodeID[key.Key256] { +func (s StringID) NodeID() kad.NodeID[kad.Key256] { return &s } @@ -65,43 +64,3 @@ func (s StringID) Equal(other string) bool { func (s StringID) String() string { return string(s) } - -type Info[K kad.Key[K], A kad.Address[A]] struct { - id *ID[K] - addrs []A -} - -var _ kad.NodeInfo[key.Key8, net.IP] = (*Info[key.Key8, net.IP])(nil) - -func NewInfo[K kad.Key[K], A kad.Address[A]](id *ID[K], addrs []A) *Info[K, A] { - return &Info[K, A]{ - id: id, - addrs: addrs, - } -} - -func (a *Info[K, A]) AddAddr(addr A) { - a.addrs = append(a.addrs, addr) -} - -func (a *Info[K, A]) RemoveAddr(addr A) { - writeIndex := 0 - // remove all occurrences of addr - for _, ad := range a.addrs { - if !ad.Equal(addr) { - a.addrs[writeIndex] = ad - writeIndex++ - } - } - a.addrs = a.addrs[:writeIndex] -} - -func (a *Info[K, A]) ID() kad.NodeID[K] { - return a.id -} - -func (a *Info[K, A]) Addresses() []A { - addresses := make([]A, len(a.addrs)) - copy(addresses, a.addrs) - return addresses -} diff --git a/internal/kadtest/message.go b/kad/kadtest/message.go similarity index 91% rename from internal/kadtest/message.go rename to kad/kadtest/message.go index 05071ce..e866650 100644 --- a/internal/kadtest/message.go +++ b/kad/kadtest/message.go @@ -1,14 +1,15 @@ package kadtest import ( - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" + "github.com/libp2p/go-libdht" + "github.com/libp2p/go-libdht/kad" + "github.com/libp2p/go-libdht/key" ) // StrAddr is a simple implementation of kad.Address that uses a string to represent the address. type StrAddr string -var _ kad.Address[StrAddr] = StrAddr("") +var _ libdht.Address[StrAddr] = StrAddr("") func (a StrAddr) Equal(b StrAddr) bool { return a == b } diff --git a/internal/kadtest/rand.go b/kad/kadtest/rand.go similarity index 96% rename from internal/kadtest/rand.go rename to kad/kadtest/rand.go index 7c4be43..425b53a 100644 --- a/internal/kadtest/rand.go +++ b/kad/kadtest/rand.go @@ -4,7 +4,7 @@ import ( "math/rand" "strconv" - "github.com/plprobelab/go-kademlia/key" + "github.com/libp2p/go-libdht/key" ) var rng = rand.New(rand.NewSource(299792458)) diff --git a/kad/key.go b/kad/key.go new file mode 100644 index 0000000..53466cd --- /dev/null +++ b/kad/key.go @@ -0,0 +1,155 @@ +package kad + +import ( + "bytes" + "encoding/hex" + "errors" + "math" + + "github.com/libp2p/go-libdht" +) + +// Key is the interface all Kademlia key types support. +// +// A Kademlia key is defined as a bit string of arbitrary size. In practice, different Kademlia implementations use +// different key sizes. For instance, the Kademlia paper (https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) +// defines keys as 160-bits long and IPFS uses 256-bit keys. +// +// Keys are usually generated using cryptographic hash functions, however the specifics of key generation +// do not matter for key operations. +// +// A Key is not necessarily used to identify a node in the network but a derived +// representation. Implementations may choose to hash a logical node identifier +// to derive a Kademlia Key. Therefore, there also exists the concept of a NodeID +// which just defines a method to return the associated Kademlia Key. +type Key[K interface { + libdht.Distance[K] + libdht.Point[K, K] +}] interface { + libdht.Distance[K] + libdht.Point[K, K] + + // BitLen returns the length of the key in bits. + BitLen() int + + // Bit returns the value of the i'th bit of the key from most significant to least. It is equivalent to (key>>(bitlen-i-1))&1. + // Bit will panic if i is out of the range [0,BitLen()-1]. + Bit(i int) uint + + // CommonPrefixLength returns the number of leading bits the key shares with another key of the same type. + // The CommonPrefixLength of a key with itself is equal to BitLen. + CommonPrefixLength(other K) int +} + +// ErrInvalidDataLength is the error returned when attempting to construct a key from binary data of the wrong length. +var ErrInvalidDataLength = errors.New("invalid data length") + +const bitPanicMsg = "bit index out of range" + +// Key256 is a 256-bit Kademlia key. +type Key256 struct { + b *[32]byte // this is a pointer to keep the size of Key256 small since it is often passed as argument +} + +var _ Key[Key256] = Key256{} + +// NewKey256 returns a 256-bit Kademlia key whose bits are set from the supplied bytes. +func NewKey256(data []byte) Key256 { + if len(data) != 32 { + panic(ErrInvalidDataLength) + } + var b [32]byte + copy(b[:], data) + return Key256{b: &b} +} + +// ZeroKey256 returns a 256-bit Kademlia key with all bits zeroed. +func ZeroKey256() Key256 { + var b [32]byte + return Key256{b: &b} +} + +// Bit returns the value of the i'th bit of the key from most significant to least. +func (k Key256) Bit(i int) uint { + if i < 0 || i > 255 { + panic(bitPanicMsg) + } + if k.b == nil { + return 0 + } + if k.b[i/8]&(byte(1)<<(7-i%8)) == 0 { + return 0 + } else { + return 1 + } +} + +// BitLen returns the length of the key in bits, which is always 256. +func (Key256) BitLen() int { + return 256 +} + +// Distance returns the result of the eXclusive OR operation between the key and another key of the same type. +func (k Key256) Distance(o Key256) Key256 { + var xored [32]byte + if k.b != nil && o.b != nil { + for i := 0; i < 32; i++ { + xored[i] = k.b[i] ^ o.b[i] + } + } else if k.b != nil && o.b == nil { + copy(xored[:], k.b[:]) + } else if k.b == nil && o.b != nil { + copy(xored[:], o.b[:]) + } + return Key256{b: &xored} +} + +// CommonPrefixLength returns the number of leading bits the key shares with another key of the same type. +func (k Key256) CommonPrefixLength(o Key256) int { + if k.b == nil || o.b == nil { + return 256 + } + var x byte + for i := 0; i < 32; i++ { + x = k.b[i] ^ o.b[i] + if x != 0 { + return i*8 + 7 - int(math.Log2(float64(x))) // TODO: make this more efficient + } + } + return 256 +} + +// Compare compares the numeric value of the key with another key of the same type. +func (k Key256) Compare(o Key256) int { + if k.b != nil && o.b != nil { + return bytes.Compare(k.b[:], o.b[:]) + } + + var zero [32]byte + if k.b == nil { + return bytes.Compare(zero[:], o.b[:]) + } + return bytes.Compare(zero[:], k.b[:]) +} + +func (k Key256) Equal(o Key256) bool { + return k.Compare(o) == 0 +} + +// HexString returns a string containing the hexadecimal representation of the key. +func (k Key256) HexString() string { + if k.b == nil { + return "" + } + return hex.EncodeToString(k.b[:]) +} + +// MarshalBinary marshals the key into a byte slice. +// The bytes may be passed to NewKey256 to construct a new key with the same value. +func (k Key256) MarshalBinary() ([]byte, error) { + buf := make([]byte, 32) + if k.b != nil { + copy(buf, (*k.b)[:]) + } + return buf, nil +} diff --git a/kad/node.go b/kad/node.go new file mode 100644 index 0000000..4842165 --- /dev/null +++ b/kad/node.go @@ -0,0 +1,48 @@ +package kad + +import "github.com/libp2p/go-libdht" + +type NodeID[K Key[K]] interface { + libdht.NodeID[K, K] +} + +type Request[K Key[K], N NodeID[K]] interface { + libdht.Request[K, K, N] + EmptyResponse() Response[K, N] +} + +type Response[K Key[K], N NodeID[K]] interface { + libdht.Response[K, K, N] +} + +var _ NodeID[Key256] = keyID[Key256]{} +var _ Response[Key256, keyID[Key256]] = resp[Key256, keyID[Key256]]{} +var _ Request[Key256, keyID[Key256]] = req[Key256, keyID[Key256]]{} + +type keyID[K Key[K]] struct { + key K +} + +func (k keyID[K]) Key() K { + return k.key +} + +type resp[K Key[K], N NodeID[K]] struct { + peers []N +} + +func (r resp[K, N]) CloserNodes() []N { + return r.peers +} + +type req[K Key[K], N NodeID[K]] struct { + targetId N +} + +func (r req[K, N]) Target() K { + return r.targetId.Key() +} + +func (r req[K, N]) EmptyResponse() Response[K, N] { + return resp[K, N]{} +} diff --git a/kad/routing.go b/kad/routing.go new file mode 100644 index 0000000..b2f4a11 --- /dev/null +++ b/kad/routing.go @@ -0,0 +1,7 @@ +package kad + +import "github.com/libp2p/go-libdht" + +type RoutingTable[K Key[K], N NodeID[K]] interface { + libdht.RoutingTable[K, K, N] +} diff --git a/routing/simplert/README.md b/kad/simplert/README.md similarity index 100% rename from routing/simplert/README.md rename to kad/simplert/README.md diff --git a/routing/simplert/table.go b/kad/simplert/table.go similarity index 93% rename from routing/simplert/table.go rename to kad/simplert/table.go index cd4e367..c0c30f9 100644 --- a/routing/simplert/table.go +++ b/kad/simplert/table.go @@ -4,10 +4,10 @@ import ( "sort" "sync" - "github.com/plprobelab/go-kademlia/internal/kadtest" + "github.com/libp2p/go-libdht/internal/kadtest" + "github.com/libp2p/go-libdht/key" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" + "github.com/libp2p/go-libdht/kad" ) type peerInfo[K kad.Key[K], N kad.NodeID[K]] struct { @@ -23,7 +23,7 @@ type SimpleRT[K kad.Key[K], N kad.NodeID[K]] struct { buckets [][]peerInfo[K, N] } -var _ kad.RoutingTable[key.Key256, kadtest.ID[key.Key256]] = (*SimpleRT[key.Key256, kadtest.ID[key.Key256]])(nil) +var _ kad.RoutingTable[kad.Key256, kadtest.ID[kad.Key256]] = (*SimpleRT[kad.Key256, kadtest.ID[kad.Key256]])(nil) func New[K kad.Key[K], N kad.NodeID[K]](self N, bucketSize int) *SimpleRT[K, N] { rt := SimpleRT[K, N]{ @@ -142,13 +142,17 @@ func (rt *SimpleRT[K, N]) addPeer(kadId K, id N) bool { func (rt *SimpleRT[K, N]) alreadyInBucket(kadId K, bucketId int) bool { for _, p := range rt.buckets[bucketId] { // error already checked in keyError by the caller - if key.Equal(kadId, p.kadId) { + if kadId.Equal(p.kadId) { return true } } return false } +func (rt *SimpleRT[K, N]) RemoveNode(n N) bool { + return rt.RemoveKey(n.Key()) +} + func (rt *SimpleRT[K, N]) RemoveKey(kadId K) bool { rt.mu.Lock() defer rt.mu.Unlock() @@ -203,8 +207,8 @@ func (rt *SimpleRT[K, N]) NearestNodes(kadId K, n int) []N { } sort.SliceStable(peers, func(i, j int) bool { - distI := peers[i].kadId.Xor(kadId) - distJ := peers[j].kadId.Xor(kadId) + distI := peers[i].kadId.Distance(kadId) + distJ := peers[j].kadId.Distance(kadId) cmp := distI.Compare(distJ) if cmp != 0 { diff --git a/routing/simplert/table_test.go b/kad/simplert/table_test.go similarity index 76% rename from routing/simplert/table_test.go rename to kad/simplert/table_test.go index 8fb87cf..9f699fe 100644 --- a/routing/simplert/table_test.go +++ b/kad/simplert/table_test.go @@ -6,11 +6,12 @@ import ( "sync" "testing" + "github.com/libp2p/go-libdht/kad" + "github.com/libp2p/go-libdht/key" + "github.com/libp2p/go-libdht/libp2p" "github.com/libp2p/go-libp2p/core/peer" - "github.com/plprobelab/go-kademlia/libp2p" - kt "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/key" + kt "github.com/libp2p/go-libdht/internal/kadtest" "github.com/stretchr/testify/require" ) @@ -23,23 +24,23 @@ func zeroBytes(n int) []byte { } var ( - key0 = key.NewKey256(zeroBytes(32)) // 000000...000 - key1 = key.NewKey256(append([]byte{0x40}, zeroBytes(31)...)) // 010000...000 - key2 = key.NewKey256(append([]byte{0x80}, zeroBytes(31)...)) // 100000...000 - key3 = key.NewKey256(append([]byte{0xc0}, zeroBytes(31)...)) // 110000...000 - key4 = key.NewKey256(append([]byte{0xe0}, zeroBytes(31)...)) // 111000...000 - key5 = key.NewKey256(append([]byte{0x60}, zeroBytes(31)...)) // 011000...000 - key6 = key.NewKey256(append([]byte{0x70}, zeroBytes(31)...)) // 011100...000 - key7 = key.NewKey256(append([]byte{0x18}, zeroBytes(31)...)) // 000110...000 - key8 = key.NewKey256(append([]byte{0x14}, zeroBytes(31)...)) // 000101...000 - key9 = key.NewKey256(append([]byte{0x10}, zeroBytes(31)...)) // 000100...000 - key10 = key.NewKey256(append([]byte{0x20}, zeroBytes(31)...)) // 001000...000 - key11 = key.NewKey256(append([]byte{0x30}, zeroBytes(31)...)) // 001100...100 + key0 = kad.NewKey256(zeroBytes(32)) // 000000...000 + key1 = kad.NewKey256(append([]byte{0x40}, zeroBytes(31)...)) // 010000...000 + key2 = kad.NewKey256(append([]byte{0x80}, zeroBytes(31)...)) // 100000...000 + key3 = kad.NewKey256(append([]byte{0xc0}, zeroBytes(31)...)) // 110000...000 + key4 = kad.NewKey256(append([]byte{0xe0}, zeroBytes(31)...)) // 111000...000 + key5 = kad.NewKey256(append([]byte{0x60}, zeroBytes(31)...)) // 011000...000 + key6 = kad.NewKey256(append([]byte{0x70}, zeroBytes(31)...)) // 011100...000 + key7 = kad.NewKey256(append([]byte{0x18}, zeroBytes(31)...)) // 000110...000 + key8 = kad.NewKey256(append([]byte{0x14}, zeroBytes(31)...)) // 000101...000 + key9 = kad.NewKey256(append([]byte{0x10}, zeroBytes(31)...)) // 000100...000 + key10 = kad.NewKey256(append([]byte{0x20}, zeroBytes(31)...)) // 001000...000 + key11 = kad.NewKey256(append([]byte{0x30}, zeroBytes(31)...)) // 001100...100 ) func TestBasic(t *testing.T) { bucketSize := 100 - rt := New[key.Key256](kt.NewID(key0), bucketSize) + rt := New[kad.Key256](kt.NewID(key0), bucketSize) require.Equal(t, bucketSize, rt.BucketSize()) require.Equal(t, key0, rt.Self()) @@ -48,7 +49,7 @@ func TestBasic(t *testing.T) { func TestAddPeer(t *testing.T) { p := kt.NewID(key0) // irrelevant - rt := New[key.Key256](kt.NewID(key0), 2) + rt := New[kad.Key256](kt.NewID(key0), 2) require.Equal(t, 0, rt.SizeOfBucket(0)) @@ -121,7 +122,7 @@ func TestAddPeer(t *testing.T) { func TestRemoveKey(t *testing.T) { p := kt.NewID(key0) // irrelevant - rt := New[key.Key256](kt.NewID(key0), 2) + rt := New[kad.Key256](kt.NewID(key0), 2) rt.addPeer(key1, p) success := rt.RemoveKey(key2) require.False(t, success) @@ -132,7 +133,7 @@ func TestRemoveKey(t *testing.T) { func TestGetNode(t *testing.T) { p := kt.NewID(key0) - rt := New[key.Key256](kt.NewID(key0), 2) + rt := New[kad.Key256](kt.NewID(key0), 2) success := rt.addPeer(key1, p) require.True(t, success) @@ -160,12 +161,12 @@ func TestNearestPeers(t *testing.T) { bucketSize := 5 - rt := SimpleRT[key.Key256, libp2p.PeerID]{ + rt := SimpleRT[kad.Key256, libp2p.PeerID]{ self: key0, - buckets: make([][]peerInfo[key.Key256, libp2p.PeerID], 0), + buckets: make([][]peerInfo[kad.Key256, libp2p.PeerID], 0), bucketSize: bucketSize, } - rt.buckets = append(rt.buckets, make([]peerInfo[key.Key256, libp2p.PeerID], 0)) + rt.buckets = append(rt.buckets, make([]peerInfo[kad.Key256, libp2p.PeerID], 0)) rt.addPeer(key1, peerIds[1]) rt.addPeer(key2, peerIds[2]) @@ -191,21 +192,21 @@ func TestNearestPeers(t *testing.T) { // create routing table with a single duplicate peer // useful to test peers sorting with duplicate (even tough it should never happen) - rt2 := SimpleRT[key.Key256, libp2p.PeerID]{ + rt2 := SimpleRT[kad.Key256, libp2p.PeerID]{ self: key0, - buckets: make([][]peerInfo[key.Key256, libp2p.PeerID], 0), + buckets: make([][]peerInfo[kad.Key256, libp2p.PeerID], 0), bucketSize: bucketSize, } - rt2.buckets = append(rt2.buckets, make([]peerInfo[key.Key256, libp2p.PeerID], 0)) + rt2.buckets = append(rt2.buckets, make([]peerInfo[kad.Key256, libp2p.PeerID], 0)) - rt2.buckets[0] = append(rt2.buckets[0], peerInfo[key.Key256, libp2p.PeerID]{peerIds[1], key1}) - rt2.buckets[0] = append(rt2.buckets[0], peerInfo[key.Key256, libp2p.PeerID]{peerIds[1], key1}) + rt2.buckets[0] = append(rt2.buckets[0], peerInfo[kad.Key256, libp2p.PeerID]{peerIds[1], key1}) + rt2.buckets[0] = append(rt2.buckets[0], peerInfo[kad.Key256, libp2p.PeerID]{peerIds[1], key1}) peers = rt2.NearestNodes(key0, 10) require.Equal(t, peers[0], peers[1]) } func TestTableConcurrentReadWrite(t *testing.T) { - nodes := make([]*kt.ID[key.Key32], 5000) + nodes := make([]*kt.ID[kad.Key32], 5000) for i := range nodes { nodes[i] = kt.NewID(kt.RandomKey()) } diff --git a/key/trie/doc.go b/kad/trie/doc.go similarity index 100% rename from key/trie/doc.go rename to kad/trie/doc.go diff --git a/key/trie/trie.go b/kad/trie/trie.go similarity index 96% rename from key/trie/trie.go rename to kad/trie/trie.go index f1b6ece..ec88c59 100644 --- a/key/trie/trie.go +++ b/kad/trie/trie.go @@ -2,8 +2,7 @@ package trie import ( - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" + "github.com/libp2p/go-libdht/kad" ) // Trie is a trie for equal-length bit vectors, which stores values only in the leaves. @@ -90,9 +89,11 @@ func (tr *Trie[K, D]) shrink() { tr.branch[0], tr.branch[1] = nil, nil case b0.IsEmptyLeaf() && b1.IsNonEmptyLeaf(): tr.key = b1.key + tr.data = b1.data tr.branch[0], tr.branch[1] = nil, nil case b0.IsNonEmptyLeaf() && b1.IsEmptyLeaf(): tr.key = b0.key + tr.data = b0.data tr.branch[0], tr.branch[1] = nil, nil } } @@ -110,7 +111,7 @@ func (tr *Trie[K, D]) addAtDepth(depth int, kk K, data D) bool { tr.data = data return true case tr.IsNonEmptyLeaf(): - if key.Equal(*tr.key, kk) { + if (*tr.key).Equal(kk) { return false } else { p := tr.key // non-nil since IsNonEmptyLeaf @@ -140,7 +141,7 @@ func addAtDepth[K kad.Key[K], D any](depth int, tr *Trie[K, D], kk K, data D) *T case tr.IsEmptyLeaf(): return &Trie[K, D]{key: &kk, data: data} case tr.IsNonEmptyLeaf(): - eq := key.Equal(*tr.key, kk) + eq := (*tr.key).Equal(kk) if eq { return tr } @@ -181,7 +182,7 @@ func (tr *Trie[K, D]) removeAtDepth(depth int, kk K) bool { case tr.IsEmptyLeaf(): return false case tr.IsNonEmptyLeaf(): - eq := key.Equal(*tr.key, kk) + eq := (*tr.key).Equal(kk) if !eq { return false } @@ -210,7 +211,7 @@ func removeAtDepth[K kad.Key[K], D any](depth int, tr *Trie[K, D], kk K) *Trie[K case tr.IsEmptyLeaf(): return tr case tr.IsNonEmptyLeaf(): - eq := key.Equal(*tr.key, kk) + eq := (*tr.key).Equal(kk) if !eq { return tr } @@ -235,7 +236,7 @@ func Equal[K kad.Key[K], D any](a, b *Trie[K, D]) bool { case a.IsEmptyLeaf() && b.IsEmptyLeaf(): return true case a.IsNonEmptyLeaf() && b.IsNonEmptyLeaf(): - eq := key.Equal(*a.key, *b.key) + eq := (*a.key).Equal(*b.key) if !eq { return false } @@ -273,7 +274,7 @@ func findFromDepth[K kad.Key[K], D any](tr *Trie[K, D], depth int, target K) (*T case tr.IsEmptyLeaf(): return nil, depth case tr.IsNonEmptyLeaf(): - eq := key.Equal(*tr.key, target) + eq := (*tr.key).Equal(target) if !eq { return nil, depth } diff --git a/key/trie/trie_test.go b/kad/trie/trie_test.go similarity index 95% rename from key/trie/trie_test.go rename to kad/trie/trie_test.go index e962f57..e860996 100644 --- a/key/trie/trie_test.go +++ b/kad/trie/trie_test.go @@ -6,7 +6,7 @@ import ( "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" + "github.com/libp2p/go-libdht/kad" "github.com/plprobelab/go-kademlia/key" "github.com/stretchr/testify/require" ) @@ -269,6 +269,51 @@ func TestImmutableRemove(t *testing.T) { } } +func TestImmutableRemoveWithShrink(t *testing.T) { + tr := New[key.Key32, string]() + + tr.Add(key.Key32(7), "7") + tr.Add(key.Key32(6), "6") + tr.Add(key.Key32(5), "5") + tr.Add(key.Key32(4), "4") + + t.Run("shrink keeps right data", func(t *testing.T) { + trNext, err := Remove(tr, key.Key32(6)) + require.NoError(t, err) + require.Equal(t, 3, trNext.Size()) + + ok, v := Find(trNext, key.Key32(7)) + require.True(t, ok) + require.Equal(t, "7", v) + + ok, v = Find(trNext, key.Key32(5)) + require.True(t, ok) + require.Equal(t, "5", v) + + ok, v = Find(trNext, key.Key32(4)) + require.True(t, ok) + require.Equal(t, "4", v) + }) + + t.Run("shrink keeps left data", func(t *testing.T) { + trNext, err := Remove(tr, key.Key32(7)) + require.NoError(t, err) + require.Equal(t, 3, trNext.Size()) + + ok, v := Find(trNext, key.Key32(6)) + require.True(t, ok) + require.Equal(t, "6", v) + + ok, v = Find(trNext, key.Key32(5)) + require.True(t, ok) + require.Equal(t, "5", v) + + ok, v = Find(trNext, key.Key32(4)) + require.True(t, ok) + require.Equal(t, "4", v) + }) +} + func TestRemoveFromEmpty(t *testing.T) { tr := New[key.Key32, any]() removed := tr.Remove(sampleKeySet.Keys[0]) diff --git a/routing/triert/config.go b/kad/triert/config.go similarity index 91% rename from routing/triert/config.go rename to kad/triert/config.go index 5bc5baa..d816ecf 100644 --- a/routing/triert/config.go +++ b/kad/triert/config.go @@ -1,7 +1,7 @@ package triert import ( - "github.com/plprobelab/go-kademlia/kad" + "github.com/libp2p/go-libdht/kad" ) // Config holds configuration options for a TrieRT. diff --git a/routing/triert/doc.go b/kad/triert/doc.go similarity index 100% rename from routing/triert/doc.go rename to kad/triert/doc.go diff --git a/routing/triert/filter.go b/kad/triert/filter.go similarity index 90% rename from routing/triert/filter.go rename to kad/triert/filter.go index c7638de..7a889ff 100644 --- a/routing/triert/filter.go +++ b/kad/triert/filter.go @@ -1,6 +1,6 @@ package triert -import "github.com/plprobelab/go-kademlia/kad" +import "github.com/libp2p/go-libdht/kad" // KeyFilterFunc is a function that is applied before a key is added to the table. // Return false to prevent the key from being added. diff --git a/routing/triert/filter_test.go b/kad/triert/filter_test.go similarity index 91% rename from routing/triert/filter_test.go rename to kad/triert/filter_test.go index 7277398..1193559 100644 --- a/routing/triert/filter_test.go +++ b/kad/triert/filter_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/key" + "github.com/libp2p/go-libdht/internal/kadtest" + "github.com/libp2p/go-libdht/key" "github.com/stretchr/testify/require" ) diff --git a/routing/triert/table.go b/kad/triert/table.go similarity index 92% rename from routing/triert/table.go rename to kad/triert/table.go index dc58e39..fc0fb8f 100644 --- a/routing/triert/table.go +++ b/kad/triert/table.go @@ -5,10 +5,9 @@ import ( "sync" "sync/atomic" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/key/trie" + "github.com/libp2p/go-libdht/internal/kadtest" + "github.com/libp2p/go-libdht/kad" + "github.com/libp2p/go-libdht/key/trie" ) // TrieRT is a routing table backed by a XOR Trie which offers good scalablity and performance @@ -21,7 +20,7 @@ type TrieRT[K kad.Key[K], N kad.NodeID[K]] struct { trie atomic.Value // holds a *trie.Trie[K, N] } -var _ kad.RoutingTable[key.Key256, kadtest.ID[key.Key256]] = (*TrieRT[key.Key256, kadtest.ID[key.Key256]])(nil) +var _ kad.RoutingTable[kad.Key256, kadtest.ID[kad.Key256]] = (*TrieRT[kad.Key256, kadtest.ID[kad.Key256]])(nil) // New creates a new TrieRT using the supplied key as the local node's Kademlia key. // If cfg is nil, the default config is used. @@ -70,6 +69,10 @@ func (rt *TrieRT[K, N]) AddNode(node N) bool { return true } +func (rt *TrieRT[K, N]) RemoveNode(n N) bool { + return rt.RemoveKey(n.Key()) +} + // RemoveKey tries to remove a node identified by its Kademlia key from the // routing table. It returns true if the key was found to be present in the table and was removed. func (rt *TrieRT[K, N]) RemoveKey(kk K) bool { diff --git a/routing/triert/table_test.go b/kad/triert/table_test.go similarity index 99% rename from routing/triert/table_test.go rename to kad/triert/table_test.go index a29159e..8323188 100644 --- a/routing/triert/table_test.go +++ b/kad/triert/table_test.go @@ -5,9 +5,9 @@ import ( "sync" "testing" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" + "github.com/libp2p/go-libdht/internal/kadtest" + "github.com/libp2p/go-libdht/kad" + "github.com/libp2p/go-libdht/key" "github.com/stretchr/testify/require" ) diff --git a/kaderr/errors.go b/kaderr/errors.go deleted file mode 100644 index a4f2714..0000000 --- a/kaderr/errors.go +++ /dev/null @@ -1,22 +0,0 @@ -package kaderr - -import "fmt" - -// A ConfigurationError is returned when a component's configuration is found to be invalid or unusable. -type ConfigurationError struct { - Component string - Err error -} - -var _ error = (*ConfigurationError)(nil) - -func (e *ConfigurationError) Error() string { - if e.Err == nil { - return fmt.Sprintf("configuration error: %s", e.Component) - } - return fmt.Sprintf("configuration error: %s: %s", e.Component, e.Err.Error()) -} - -func (e *ConfigurationError) Unwrap() error { - return e.Err -} diff --git a/key/doc.go b/key/doc.go deleted file mode 100644 index 43853dd..0000000 --- a/key/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package key provides implementations of Kademlia keys and common functions that operate on key. -package key diff --git a/key/key.go b/key/key.go deleted file mode 100644 index d75a503..0000000 --- a/key/key.go +++ /dev/null @@ -1,249 +0,0 @@ -package key - -import ( - "bytes" - "encoding/hex" - "errors" - "fmt" - "math" - - "github.com/plprobelab/go-kademlia/kad" -) - -// ErrInvalidDataLength is the error returned when attempting to construct a key from binary data of the wrong length. -var ErrInvalidDataLength = errors.New("invalid data length") - -const bitPanicMsg = "bit index out of range" - -// Key256 is a 256-bit Kademlia key. -type Key256 struct { - b *[32]byte // this is a pointer to keep the size of Key256 small since it is often passed as argument -} - -var _ kad.Key[Key256] = Key256{} - -// NewKey256 returns a 256-bit Kademlia key whose bits are set from the supplied bytes. -func NewKey256(data []byte) Key256 { - if len(data) != 32 { - panic(ErrInvalidDataLength) - } - var b [32]byte - copy(b[:], data) - return Key256{b: &b} -} - -// ZeroKey256 returns a 256-bit Kademlia key with all bits zeroed. -func ZeroKey256() Key256 { - var b [32]byte - return Key256{b: &b} -} - -// Bit returns the value of the i'th bit of the key from most significant to least. -func (k Key256) Bit(i int) uint { - if i < 0 || i > 255 { - panic(bitPanicMsg) - } - if k.b == nil { - return 0 - } - if k.b[i/8]&(byte(1)<<(7-i%8)) == 0 { - return 0 - } else { - return 1 - } -} - -// BitLen returns the length of the key in bits, which is always 256. -func (Key256) BitLen() int { - return 256 -} - -// Xor returns the result of the eXclusive OR operation between the key and another key of the same type. -func (k Key256) Xor(o Key256) Key256 { - var xored [32]byte - if k.b != nil && o.b != nil { - for i := 0; i < 32; i++ { - xored[i] = k.b[i] ^ o.b[i] - } - } else if k.b != nil && o.b == nil { - copy(xored[:], k.b[:]) - } else if k.b == nil && o.b != nil { - copy(xored[:], o.b[:]) - } - return Key256{b: &xored} -} - -// CommonPrefixLength returns the number of leading bits the key shares with another key of the same type. -func (k Key256) CommonPrefixLength(o Key256) int { - if k.b == nil || o.b == nil { - return 256 - } - var x byte - for i := 0; i < 32; i++ { - x = k.b[i] ^ o.b[i] - if x != 0 { - return i*8 + 7 - int(math.Log2(float64(x))) // TODO: make this more efficient - } - } - return 256 -} - -// Compare compares the numeric value of the key with another key of the same type. -func (k Key256) Compare(o Key256) int { - if k.b != nil && o.b != nil { - return bytes.Compare(k.b[:], o.b[:]) - } - - var zero [32]byte - if k.b == nil { - return bytes.Compare(zero[:], o.b[:]) - } - return bytes.Compare(zero[:], k.b[:]) -} - -// HexString returns a string containing the hexadecimal representation of the key. -func (k Key256) HexString() string { - if k.b == nil { - return "" - } - return hex.EncodeToString(k.b[:]) -} - -// MarshalBinary marshals the key into a byte slice. -// The bytes may be passed to NewKey256 to construct a new key with the same value. -func (k Key256) MarshalBinary() ([]byte, error) { - buf := make([]byte, 32) - if k.b != nil { - copy(buf, (*k.b)[:]) - } - return buf, nil -} - -// Key32 is a 32-bit Kademlia key, suitable for testing and simulation of small networks. -type Key32 uint32 - -var _ kad.Key[Key32] = Key32(0) - -// BitLen returns the length of the key in bits, which is always 32. -func (Key32) BitLen() int { - return 32 -} - -// Bit returns the value of the i'th bit of the key from most significant to least. -func (k Key32) Bit(i int) uint { - if i < 0 || i > 31 { - panic(bitPanicMsg) - } - return uint((k >> (31 - i)) & 1) -} - -// Xor returns the result of the eXclusive OR operation between the key and another key of the same type. -func (k Key32) Xor(o Key32) Key32 { - return k ^ o -} - -// CommonPrefixLength returns the number of leading bits the key shares with another key of the same type. -func (k Key32) CommonPrefixLength(o Key32) int { - a := uint32(k) - b := uint32(o) - for i := 32; i > 0; i-- { - if a == b { - return i - } - a >>= 1 - b >>= 1 - } - return 0 -} - -// Compare compares the numeric value of the key with another key of the same type. -func (k Key32) Compare(o Key32) int { - if k < o { - return -1 - } else if k > o { - return 1 - } - return 0 -} - -// HexString returns a string containing the hexadecimal representation of the key. -func (k Key32) HexString() string { - return fmt.Sprintf("%04x", uint32(k)) -} - -// BitString returns a string containing the binary representation of the key. -func (k Key32) BitString() string { - return fmt.Sprintf("%032b", uint32(k)) -} - -func (k Key32) String() string { - return k.HexString() -} - -// Key8 is an 8-bit Kademlia key, suitable for testing and simulation of very small networks. -type Key8 uint8 - -var _ kad.Key[Key8] = Key8(0) - -// BitLen returns the length of the key in bits, which is always 8. -func (Key8) BitLen() int { - return 8 -} - -// Bit returns the value of the i'th bit of the key from most significant to least. -func (k Key8) Bit(i int) uint { - if i < 0 || i > 7 { - panic(bitPanicMsg) - } - return uint((k >> (7 - i)) & 1) -} - -// Xor returns the result of the eXclusive OR operation between the key and another key of the same type. -func (k Key8) Xor(o Key8) Key8 { - return k ^ o -} - -// CommonPrefixLength returns the number of leading bits the key shares with another key of the same type. -func (k Key8) CommonPrefixLength(o Key8) int { - a := uint8(k) - b := uint8(o) - for i := 8; i > 0; i-- { - if a == b { - return i - } - a >>= 1 - b >>= 1 - } - return 0 -} - -// Compare compares the numeric value of the key with another key of the same type. -func (k Key8) Compare(o Key8) int { - if k < o { - return -1 - } else if k > o { - return 1 - } - return 0 -} - -// HexString returns a string containing the hexadecimal representation of the key. -func (k Key8) HexString() string { - return fmt.Sprintf("%x", uint8(k)) -} - -func (k Key8) String() string { - return k.HexString() -} - -// HexString returns a string containing the binary representation of the key. -func (k Key8) BitString() string { - return fmt.Sprintf("%08b", uint8(k)) -} - -// KeyList is a list of Kademlia keys. It implements sort.Interface. -type KeyList[K kad.Key[K]] []K - -func (ks KeyList[K]) Len() int { return len(ks) } -func (ks KeyList[K]) Swap(i, j int) { ks[i], ks[j] = ks[j], ks[i] } -func (ks KeyList[K]) Less(i, j int) bool { return ks[i].Compare(ks[j]) < 0 } diff --git a/key/key_test.go b/key/key_test.go deleted file mode 100644 index 86b6baa..0000000 --- a/key/key_test.go +++ /dev/null @@ -1,362 +0,0 @@ -package key - -import ( - "fmt" - "strconv" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/kad" -) - -func TestKey256(t *testing.T) { - tester := &KeyTester[Key256]{ - // kt.Key0 is 00000...000 - Key0: ZeroKey256(), - - // key1 is key0 + 1 (00000...001) - Key1: NewKey256([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}), - - // key2 is key0 + 2 (00000...010) - Key2: NewKey256([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}), - - // key1xor2 is key1 ^ key2 (00000...011) - Key1xor2: NewKey256([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03}), - - // key100 is key0 with the most significant bit set (10000...000) - Key100: NewKey256([]byte{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), - - // key010 is key0 with the second most significant bit set (01000...000) - Key010: NewKey256([]byte{0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), - - KeyX: NewKey256([]byte{0x23, 0xe4, 0xdd, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), - } - - tester.RunTests(t) - - testBinaryMarshaler(t, tester.KeyX, NewKey256) -} - -func TestKey32(t *testing.T) { - tester := &KeyTester[Key32]{ - Key0: Key32(0), - Key1: Key32(1), - Key2: Key32(2), - Key1xor2: Key32(3), - Key100: Key32(0x80000000), - Key010: Key32(0x40000000), - KeyX: Key32(0x23e4dd03), - } - - tester.RunTests(t) -} - -func TestKey8(t *testing.T) { - tester := &KeyTester[Key8]{ - Key0: Key8(0), - Key1: Key8(1), - Key2: Key8(2), - Key1xor2: Key8(3), - Key100: Key8(0x80), - Key010: Key8(0x40), - KeyX: Key8(0x23), - } - - tester.RunTests(t) -} - -// TestBitStrKey7 tests a strange 7-bit Kademlia key -func TestBitStrKey7(t *testing.T) { - tester := &KeyTester[BitStrKey]{ - Key0: BitStrKey("0000000"), - Key1: BitStrKey("0000001"), - Key2: BitStrKey("0000010"), - Key1xor2: BitStrKey("0000011"), - Key100: BitStrKey("1000000"), - Key010: BitStrKey("0100000"), - KeyX: BitStrKey("1010110"), - } - - tester.RunTests(t) -} - -// KeyTester tests a kad.Key's implementation -type KeyTester[K kad.Key[K]] struct { - // Key 0 is zero - Key0 K - - // Key1 is Key0 + 1 (00000...001) - Key1 K - - // Key2 is Key0 + 2 (00000...010) - Key2 K - - // Key1xor2 is Key1 ^ Key2 (00000...011) - Key1xor2 K - - // Key100 is Key0 with the most significant bit set (10000...000) - Key100 K - - // Key010 is Key0 with the second most significant bit set (01000...000) - Key010 K - - // KeyX is a random key - KeyX K -} - -func (kt *KeyTester[K]) RunTests(t *testing.T) { - t.Helper() - t.Run("Xor", kt.TestXor) - t.Run("CommonPrefixLength", kt.TestCommonPrefixLength) - t.Run("Compare", kt.TestCompare) - t.Run("Bit", kt.TestBit) - t.Run("BitString", kt.TestBitString) - t.Run("HexString", kt.TestHexString) -} - -func (kt *KeyTester[K]) TestXor(t *testing.T) { - xored := kt.Key0.Xor(kt.Key0) - require.Equal(t, kt.Key0, xored) - - xored = kt.KeyX.Xor(kt.Key0) - require.Equal(t, kt.KeyX, xored) - - xored = kt.Key0.Xor(kt.KeyX) - require.Equal(t, kt.KeyX, xored) - - xored = kt.KeyX.Xor(kt.KeyX) - require.Equal(t, kt.Key0, xored) - - xored = kt.Key1.Xor(kt.Key2) - require.Equal(t, kt.Key1xor2, xored) - - var empty K // zero value of key - xored = kt.Key0.Xor(empty) - require.Equal(t, kt.Key0, xored) - xored = empty.Xor(kt.Key0) - require.Equal(t, kt.Key0, xored) -} - -func (kt *KeyTester[K]) TestCommonPrefixLength(t *testing.T) { - cpl := kt.Key0.CommonPrefixLength(kt.Key0) - require.Equal(t, kt.Key0.BitLen(), cpl) - - cpl = kt.Key0.CommonPrefixLength(kt.Key1) - require.Equal(t, kt.Key0.BitLen()-1, cpl) - - cpl = kt.Key0.CommonPrefixLength(kt.Key100) - require.Equal(t, 0, cpl) - - cpl = kt.Key0.CommonPrefixLength(kt.Key010) - require.Equal(t, 1, cpl) - - var empty K // zero value of key - cpl = kt.Key0.CommonPrefixLength(empty) - require.Equal(t, kt.Key0.BitLen(), cpl) - cpl = empty.CommonPrefixLength(kt.Key0) - require.Equal(t, kt.Key0.BitLen(), cpl) -} - -func (kt *KeyTester[K]) TestCompare(t *testing.T) { - res := kt.Key0.Compare(kt.Key0) - require.Equal(t, 0, res) - - res = kt.Key0.Compare(kt.Key1) - require.Equal(t, -1, res) - - res = kt.Key0.Compare(kt.Key2) - require.Equal(t, -1, res) - - res = kt.Key0.Compare(kt.Key100) - require.Equal(t, -1, res) - - res = kt.Key0.Compare(kt.Key010) - require.Equal(t, -1, res) - - res = kt.Key1.Compare(kt.Key1) - require.Equal(t, 0, res) - - res = kt.Key1.Compare(kt.Key0) - require.Equal(t, 1, res) - - res = kt.Key1.Compare(kt.Key2) - require.Equal(t, -1, res) - - var empty K // zero value of key - res = kt.Key0.Compare(empty) - require.Equal(t, 0, res) - res = empty.Compare(kt.Key0) - require.Equal(t, 0, res) -} - -func (kt *KeyTester[K]) TestBit(t *testing.T) { - for i := 0; i < kt.Key0.BitLen(); i++ { - require.Equal(t, uint(0), kt.Key0.Bit(i), fmt.Sprintf("Key0.Bit(%d)=%d", i, kt.Key0.Bit(i))) - } - - for i := 0; i < kt.Key1.BitLen()-1; i++ { - require.Equal(t, uint(0), kt.Key1.Bit(i), fmt.Sprintf("Key1.Bit(%d)=%d", i, kt.Key1.Bit(i))) - } - require.Equal(t, uint(1), kt.Key1.Bit(kt.Key0.BitLen()-1), fmt.Sprintf("Key1.Bit(%d)=%d", kt.Key1.BitLen()-1, kt.Key1.Bit(kt.Key1.BitLen()-1))) - - for i := 0; i < kt.Key0.BitLen()-2; i++ { - require.Equal(t, uint(0), kt.Key2.Bit(i), fmt.Sprintf("Key1.Bit(%d)=%d", i, kt.Key2.Bit(i))) - } - require.Equal(t, uint(1), kt.Key2.Bit(kt.Key2.BitLen()-2), fmt.Sprintf("Key1.Bit(%d)=%d", kt.Key2.BitLen()-2, kt.Key2.BitLen()-2)) - require.Equal(t, uint(0), kt.Key2.Bit(kt.Key2.BitLen()-1), fmt.Sprintf("Key1.Bit(%d)=%d", kt.Key2.BitLen()-2, kt.Key2.BitLen()-1)) - - var empty K // zero value of key - for i := 0; i < empty.BitLen(); i++ { - require.Equal(t, uint(0), empty.Bit(i), fmt.Sprintf("empty.Bit(%d)=%d", i, kt.Key0.Bit(i))) - } -} - -func (kt *KeyTester[K]) TestBitString(t *testing.T) { - str := BitString(kt.KeyX) - t.Logf("BitString(%v)=%s", kt.KeyX, str) - for i := 0; i < kt.KeyX.BitLen(); i++ { - expected := byte('0') - if kt.KeyX.Bit(i) == 1 { - expected = byte('1') - } - require.Equal(t, string(expected), string(str[i])) - } -} - -func (kt *KeyTester[K]) TestHexString(t *testing.T) { - str := HexString(kt.KeyX) - t.Logf("HexString(%v)=%s", kt.KeyX, str) - - bitpos := kt.KeyX.BitLen() - 1 - - for i := len(str) - 1; i >= 0; i-- { - v, err := strconv.ParseInt(string(str[i]), 16, 8) - require.NoError(t, err) - mask := uint(0x1) - for b := 0; b < 4; b++ { - got := (uint(v) & mask) >> b - want := kt.KeyX.Bit(bitpos) - require.Equal(t, want, got, fmt.Sprintf("bit %d: (%04b & %04b)>>%d = %d, wanted kt.KeyX.Bit(%d)=%d", bitpos, uint(v), b, mask, (uint(v)&mask), bitpos, want)) - bitpos-- - if bitpos < 0 { - break - } - mask <<= 1 - } - - if bitpos < 0 && i > 0 { - t.Errorf("hex string had length %d, but expected %d", len(str), (kt.KeyX.BitLen()+3)/4) - break - } - } - - if bitpos >= 0 { - t.Errorf("hex string had length %d, but expected %d", len(str), (kt.KeyX.BitLen()+3)/4) - } -} - -// testBinaryMarshaler tests the behaviour of a kad.Key implementation that also implements the BinaryMarshaler interface -func testBinaryMarshaler[K interface { - kad.Key[K] - MarshalBinary() ([]byte, error) -}](t *testing.T, k K, newFunc func([]byte) K, -) { - b, err := k.MarshalBinary() - require.NoError(t, err) - - other := newFunc(b) - - res := k.Compare(other) - require.Equal(t, 0, res) -} - -// BitStrKey is a key represented by a string of 1's and 0's -type BitStrKey string - -var _ kad.Key[BitStrKey] = BitStrKey("1010") - -func (k BitStrKey) BitLen() int { - return len(k) -} - -func (k BitStrKey) Bit(i int) uint { - if i < 0 || i > len(k) { - panic(bitPanicMsg) - } - if k[i] == '1' { - return 1 - } else if k[i] == '0' { - return 0 - } - panic("BitStrKey: not a binary string") -} - -func (k BitStrKey) Xor(o BitStrKey) BitStrKey { - if len(k) != len(o) { - if len(k) == 0 && o.isZero() { - return BitStrKey(o) - } - if len(o) == 0 && k.isZero() { - return BitStrKey(k) - } - panic("BitStrKey: other key has different length") - } - buf := make([]byte, len(k)) - for i := range buf { - if k[i] != o[i] { - buf[i] = '1' - } else { - buf[i] = '0' - } - } - return BitStrKey(string(buf)) -} - -func (k BitStrKey) CommonPrefixLength(o BitStrKey) int { - if len(k) != len(o) { - if len(k) == 0 && o.isZero() { - return len(o) - } - if len(o) == 0 && k.isZero() { - return len(k) - } - panic("BitStrKey: other key has different length") - } - for i := 0; i < len(k); i++ { - if k[i] != o[i] { - return i - } - } - return len(k) -} - -func (k BitStrKey) Compare(o BitStrKey) int { - if len(k) != len(o) { - if len(k) == 0 && o.isZero() { - return 0 - } - if len(o) == 0 && k.isZero() { - return 0 - } - panic("BitStrKey: other key has different length") - } - for i := 0; i < len(k); i++ { - if k[i] != o[i] { - if k[i] < o[i] { - return -1 - } - return 1 - } - } - return 0 -} - -func (k BitStrKey) isZero() bool { - for i := 0; i < len(k); i++ { - if k[i] != '0' { - return false - } - } - return true -} diff --git a/key/op.go b/key/op.go deleted file mode 100644 index 2c5507e..0000000 --- a/key/op.go +++ /dev/null @@ -1,68 +0,0 @@ -package key - -import ( - "sort" - "strings" - - "github.com/plprobelab/go-kademlia/kad" -) - -// Equal reports whether two keys have equal numeric values. -func Equal[K kad.Key[K]](a, b K) bool { - return a.Compare(b) == 0 -} - -// BitString returns a string containing the binary representation of a key. -func BitString[K kad.Key[K]](k K) string { - if bs, ok := any(k).(interface{ BitString() string }); ok { - return bs.BitString() - } - b := new(strings.Builder) - b.Grow(k.BitLen()) - for i := 0; i < k.BitLen(); i++ { - if k.Bit(i) == 0 { - b.WriteByte('0') - } else { - b.WriteByte('1') - } - } - return b.String() -} - -// HexString returns a string containing the hexadecimal representation of a key. -func HexString[K kad.Key[K]](k K) string { - if hs, ok := any(k).(interface{ HexString() string }); ok { - return hs.HexString() - } - b := new(strings.Builder) - b.Grow((k.BitLen() + 3) / 4) - - h := [...]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'} - - prebits := k.BitLen() % 4 - if prebits > 0 { - // deal with initial nibble for key lengths that aren't a multiple of 4 - var n byte - n |= byte(k.Bit(0)) - for i := 1; i < prebits; i++ { - n <<= 1 - n |= byte(k.Bit(i)) - } - b.WriteByte(h[n]) - } - - for i := prebits; i < k.BitLen(); i += 4 { - var n byte - n |= byte(k.Bit(i)) << 3 - n |= byte(k.Bit(i+1)) << 2 - n |= byte(k.Bit(i+2)) << 1 - n |= byte(k.Bit(i + 3)) - b.WriteByte(h[n]) - } - return b.String() -} - -// IsSorted reports whether a list of keys is sorted in ascending numerical order. -func IsSorted[K kad.Key[K]](ks []K) bool { - return sort.IsSorted(KeyList[K](ks)) -} diff --git a/libp2p/Makefile b/libp2p/Makefile deleted file mode 100644 index 21540f6..0000000 --- a/libp2p/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -PB = $(wildcard *.proto) -GO = $(PB:.proto=.pb.go) - -all: $(GO) - -%.pb.go: %.proto - protoc --go_out=. ./$(PB) - -clean: - rm -f *.pb.go - rm -f *.go diff --git a/libp2p/README.md b/libp2p/README.md deleted file mode 100644 index 0c81c95..0000000 --- a/libp2p/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Libp2p Endpoint - -Author: [Guillaume Michel](https://github.com/guillaumemichel) - -`Libp2pEndpoint` is a message endpoint using Libp2p to exchange messages between Kademlia nodes over a network. It makes use of the Libp2p peerstore to record peers. The implementation is multi thread, as it is a requirement from Libp2p. - -When sending a Kademlia request, a new go routine is created to send the request and wait for the response. Once the response is received, the go routine will add a new `Action` to handle the received response to the `Scheduler`'s event queue and dies. The single worker will pick the response handling `Action` from the `Scheduler` once it is available. - -When in `Server` mode, Libp2p stream handlers are added to the Libp2p `host`. Once a new request is caught by the Libp2p stream handler, it is sent to the `Scheduler`'s event queue, and handled by the single worker. \ No newline at end of file diff --git a/libp2p/addrinfo.go b/libp2p/addrinfo.go deleted file mode 100644 index 2f218c6..0000000 --- a/libp2p/addrinfo.go +++ /dev/null @@ -1,45 +0,0 @@ -package libp2p - -import ( - "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" -) - -type AddrInfo struct { - peer.AddrInfo - id *PeerID -} - -var _ kad.NodeInfo[key.Key256, multiaddr.Multiaddr] = (*AddrInfo)(nil) - -func NewAddrInfo(ai peer.AddrInfo) *AddrInfo { - return &AddrInfo{ - AddrInfo: ai, - id: NewPeerID(ai.ID), - } -} - -func (ai AddrInfo) Key() key.Key256 { - return ai.id.Key() -} - -func (ai AddrInfo) String() string { - return ai.id.String() -} - -func (ai AddrInfo) PeerID() *PeerID { - return ai.id -} - -func (ai AddrInfo) ID() kad.NodeID[key.Key256] { - return ai.id -} - -func (ai AddrInfo) Addresses() []multiaddr.Multiaddr { - addrs := make([]multiaddr.Multiaddr, len(ai.Addrs)) - copy(addrs, ai.Addrs) - return addrs -} diff --git a/libp2p/errors.go b/libp2p/errors.go deleted file mode 100644 index 56f39ca..0000000 --- a/libp2p/errors.go +++ /dev/null @@ -1,10 +0,0 @@ -package libp2p - -import "errors" - -var ( - ErrNotPeerAddrInfo = errors.New("not peer.AddrInfo") - ErrRequirePeerID = errors.New("Libp2pEndpoint requires peer.ID") - ErrRequireProtoKadMessage = errors.New("Libp2pEndpoint requires ProtoKadMessage") - ErrRequireProtoKadResponse = errors.New("Libp2pEndpoint requires ProtoKadResponseMessage") -) diff --git a/libp2p/helpers.go b/libp2p/helpers.go deleted file mode 100644 index b5a645b..0000000 --- a/libp2p/helpers.go +++ /dev/null @@ -1,112 +0,0 @@ -package libp2p - -import ( - "errors" - - "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/endpoint" -) - -var ErrNoValidAddresses = errors.New("no valid addresses") - -func FindPeerRequest(p *PeerID) *Message { - marshalledPeerid, _ := p.MarshalBinary() - return &Message{ - Type: Message_FIND_NODE, - Key: marshalledPeerid, - } -} - -func FindPeerResponse(peers []kad.NodeID[key.Key256], e endpoint.NetworkedEndpoint[key.Key256, multiaddr.Multiaddr]) *Message { - return &Message{ - Type: Message_FIND_NODE, - CloserPeers: NodeIDsToPbPeers(peers, e), - } -} - -func (msg *Message) Target() key.Key256 { - p, err := peer.IDFromBytes(msg.GetKey()) - if err != nil { - return key.ZeroKey256() - } - return PeerID{ID: p}.Key() -} - -func (msg *Message) EmptyResponse() kad.Response[key.Key256, multiaddr.Multiaddr] { - return &Message{} -} - -func (msg *Message) CloserNodes() []kad.NodeInfo[key.Key256, multiaddr.Multiaddr] { - closerPeers := msg.GetCloserPeers() - if closerPeers == nil { - return []kad.NodeInfo[key.Key256, multiaddr.Multiaddr]{} - } - return ParsePeers(closerPeers) -} - -func PBPeerToPeerInfo(pbp *Message_Peer) (*AddrInfo, error) { - addrs := make([]multiaddr.Multiaddr, 0, len(pbp.Addrs)) - for _, a := range pbp.Addrs { - addr, err := multiaddr.NewMultiaddrBytes(a) - if err == nil { - addrs = append(addrs, addr) - } - } - if len(addrs) == 0 { - return nil, ErrNoValidAddresses - } - - return NewAddrInfo(peer.AddrInfo{ - ID: peer.ID(pbp.Id), - Addrs: addrs, - }), nil -} - -func ParsePeers(pbps []*Message_Peer) []kad.NodeInfo[key.Key256, multiaddr.Multiaddr] { - peers := make([]kad.NodeInfo[key.Key256, multiaddr.Multiaddr], 0, len(pbps)) - for _, p := range pbps { - pi, err := PBPeerToPeerInfo(p) - if err == nil { - peers = append(peers, pi) - } - } - return peers -} - -func NodeIDsToPbPeers(peers []kad.NodeID[key.Key256], e endpoint.NetworkedEndpoint[key.Key256, multiaddr.Multiaddr]) []*Message_Peer { - if len(peers) == 0 || e == nil { - return nil - } - - pbPeers := make([]*Message_Peer, 0, len(peers)) - for _, n := range peers { - p := n.(*PeerID) - - id, err := e.NetworkAddress(n) - if err != nil { - continue - } - // convert NetworkAddress to []multiaddr.Multiaddr - addrs := id.(*AddrInfo).Addrs - pbAddrs := make([][]byte, len(addrs)) - // convert multiaddresses to bytes - for i, a := range addrs { - pbAddrs[i] = a.Bytes() - } - - connectedness, err := e.Connectedness(n) - if err != nil { - continue - } - pbPeers = append(pbPeers, &Message_Peer{ - Id: []byte(p.ID), - Addrs: pbAddrs, - Connection: Message_ConnectionType(connectedness), - }) - } - return pbPeers -} diff --git a/libp2p/helpers_test.go b/libp2p/helpers_test.go deleted file mode 100644 index 63b43f3..0000000 --- a/libp2p/helpers_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package libp2p - -import ( - "context" - "strconv" - "testing" - "time" - - "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/sim" -) - -var ( - _ ProtoKadRequestMessage[key.Key256, multiaddr.Multiaddr] = (*Message)(nil) - _ ProtoKadResponseMessage[key.Key256, multiaddr.Multiaddr] = (*Message)(nil) -) - -var testPeerstoreTTL = 10 * time.Minute - -func TestFindPeerRequest(t *testing.T) { - p, err := peer.Decode("12D3KooWH6Qd1EW75ANiCtYfD51D6M7MiZwLQ4g8wEBpoEUnVYNz") - require.NoError(t, err) - - pid := NewPeerID(p) - msg := FindPeerRequest(pid) - - require.Equal(t, msg.GetKey(), []byte(p)) - - b := key.Equal(msg.Target(), pid.Key()) - require.True(t, b) - - require.Equal(t, 0, len(msg.CloserNodes())) -} - -func createDummyPeerInfo(id, addr string) (*AddrInfo, error) { - p, err := peer.Decode(id) - if err != nil { - return nil, err - } - a, err := multiaddr.NewMultiaddr(addr) - if err != nil { - return nil, err - } - return NewAddrInfo(peer.AddrInfo{ - ID: p, - Addrs: []multiaddr.Multiaddr{a}, - }), nil -} - -func TestFindPeerResponse(t *testing.T) { - ctx := context.Background() - selfAddr, err := createDummyPeerInfo("12BoooooSELF", "/ip4/1.1.1.1") - require.NoError(t, err) - - fakeEndpoint := sim.NewEndpoint[key.Key256, multiaddr.Multiaddr](selfAddr, nil, nil) - - nPeers := 5 - closerPeers := make([]kad.NodeInfo[key.Key256, multiaddr.Multiaddr], nPeers) - closerIds := make([]kad.NodeID[key.Key256], nPeers) - for i := 0; i < nPeers; i++ { - s := strconv.Itoa(2 + i) - closerPeers[i], err = createDummyPeerInfo("12BooooPEER"+s, "/ip4/"+s+"."+s+"."+s+"."+s) - require.NoError(t, err) - - closerIds[i] = closerPeers[i].(*AddrInfo).PeerID() - fakeEndpoint.MaybeAddToPeerstore(ctx, closerPeers[i], testPeerstoreTTL) - } - - resp := FindPeerResponse(closerIds, fakeEndpoint) - - // require.Nil(t, resp.Target()) - require.Equal(t, closerPeers, resp.CloserNodes()) -} - -func TestCornerCases(t *testing.T) { - resp := FindPeerResponse(nil, nil) - // require.Nil(t, resp.Target()) - require.Equal(t, 0, len(resp.CloserNodes())) - - require.Equal(t, &Message{}, resp.EmptyResponse()) - - ids := make([]kad.NodeID[key.Key256], 0) - resp = FindPeerResponse(ids, nil) - - // require.Nil(t, resp.Target()) - require.Equal(t, 0, len(resp.CloserNodes())) - - fakeEndpoint := sim.NewEndpoint[key.Key256, multiaddr.Multiaddr](AddrInfo{}, nil, nil) - n0, err := peer.Decode("1D3oooUnknownPeer") - require.NoError(t, err) - ids = append(ids, &PeerID{ID: n0}) - - resp = FindPeerResponse(ids, fakeEndpoint) - require.Equal(t, 0, len(resp.CloserNodes())) - - pbp := Message_Peer{ - Id: []byte(n0), - Addrs: [][]byte{}, - Connection: 0, - } - - ai, err := PBPeerToPeerInfo(&pbp) - require.Equal(t, err, ErrNoValidAddresses) - require.Nil(t, ai) -} diff --git a/libp2p/libp2pendpoint.go b/libp2p/libp2pendpoint.go deleted file mode 100644 index a0f0465..0000000 --- a/libp2p/libp2pendpoint.go +++ /dev/null @@ -1,374 +0,0 @@ -package libp2p - -import ( - "context" - "errors" - "fmt" - "io" - "sync" - "time" - - "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/network" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/protocol" - "github.com/libp2p/go-msgio/pbio" - "github.com/multiformats/go-multiaddr" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" - "github.com/plprobelab/go-kademlia/util" -) - -type DialReportFn func(context.Context, bool) - -// TODO: Use sync.Pool to reuse buffers https://pkg.go.dev/sync#Pool - -type Libp2pEndpoint struct { - ctx context.Context - host host.Host - sched event.Scheduler - - // peer filters to be applied before adding peer to peerstore - - writers sync.Pool - readers sync.Pool -} - -var ( - _ endpoint.NetworkedEndpoint[key.Key256, multiaddr.Multiaddr] = (*Libp2pEndpoint)(nil) - _ endpoint.ServerEndpoint[key.Key256, multiaddr.Multiaddr] = (*Libp2pEndpoint)(nil) -) - -func NewLibp2pEndpoint(ctx context.Context, host host.Host, - sched event.Scheduler, -) *Libp2pEndpoint { - return &Libp2pEndpoint{ - ctx: ctx, - host: host, - sched: sched, - writers: sync.Pool{}, - readers: sync.Pool{}, - } -} - -func getPeerID(id kad.NodeID[key.Key256]) (*PeerID, error) { - if p, ok := id.(*PeerID); ok { - return p, nil - } - return nil, endpoint.ErrInvalidPeer -} - -func (e *Libp2pEndpoint) AsyncDialAndReport(ctx context.Context, - id kad.NodeID[key.Key256], reportFn DialReportFn, -) error { - p, err := getPeerID(id) - if err != nil { - return err - } - if e.host.Network().Connectedness(p.ID) == network.Connected { - // if peer is already connected, no need to dial - if reportFn != nil { - reportFn(ctx, true) - } - return nil - } - go func() { - ctx, span := util.StartSpan(ctx, "Libp2pEndpoint.AsyncDialAndReport", - trace.WithAttributes(attribute.String("PeerID", p.String()))) - defer span.End() - - success := true - if err := e.DialPeer(ctx, p); err != nil { - span.AddEvent("dial failed", trace.WithAttributes( - attribute.String("Error", err.Error()), - )) - success = false - } else { - span.AddEvent("dial successful") - } - - if reportFn != nil { - // report dial result where it is needed - e.sched.EnqueueAction(ctx, event.BasicAction(func(ctx context.Context) { - reportFn(ctx, success) - })) - } - }() - return nil -} - -func (e *Libp2pEndpoint) DialPeer(ctx context.Context, id kad.NodeID[key.Key256]) error { - p, err := getPeerID(id) - if err != nil { - return err - } - - _, span := util.StartSpan(ctx, "Libp2pEndpoint.DialPeer", trace.WithAttributes( - attribute.String("PeerID", p.String()), - )) - defer span.End() - - if e.host.Network().Connectedness(p.ID) == network.Connected { - span.AddEvent("Already connected") - return nil - } - - pi := peer.AddrInfo{ID: p.ID} - if err := e.host.Connect(ctx, pi); err != nil { - span.AddEvent("Connection failed", trace.WithAttributes( - attribute.String("Error", err.Error()), - )) - return err - } - span.AddEvent("Connection successful") - return nil -} - -func (e *Libp2pEndpoint) MaybeAddToPeerstore(ctx context.Context, - id kad.NodeInfo[key.Key256, multiaddr.Multiaddr], ttl time.Duration, -) error { - _, span := util.StartSpan(ctx, "Libp2pEndpoint.MaybeAddToPeerstore", - trace.WithAttributes(attribute.String("PeerID", id.ID().String()))) - defer span.End() - - ai, ok := id.(*AddrInfo) - if !ok { - return endpoint.ErrInvalidPeer - } - - // Don't add addresses for self or our connected peers. We have better ones. - if ai.PeerID().ID == e.host.ID() || - e.host.Network().Connectedness(ai.PeerID().ID) == network.Connected { - return nil - } - e.host.Peerstore().AddAddrs(ai.PeerID().ID, ai.Addrs, ttl) - return nil -} - -func (e *Libp2pEndpoint) SendRequestHandleResponse(ctx context.Context, - protoID address.ProtocolID, n kad.NodeID[key.Key256], req kad.Message, - resp kad.Message, timeout time.Duration, - responseHandlerFn endpoint.ResponseHandlerFn[key.Key256, multiaddr.Multiaddr], -) error { - _, span := util.StartSpan(ctx, - "Libp2pEndpoint.SendRequestHandleResponse", trace.WithAttributes( - attribute.String("PeerID", n.String()), - )) - defer span.End() - - protoResp, ok := resp.(ProtoKadResponseMessage[key.Key256, multiaddr.Multiaddr]) - if !ok { - span.RecordError(ErrRequireProtoKadResponse) - return ErrRequireProtoKadResponse - } - - protoReq, ok := req.(ProtoKadMessage) - if !ok { - span.RecordError(ErrRequireProtoKadMessage) - return ErrRequireProtoKadMessage - } - - p, ok := n.(*PeerID) - if !ok { - span.RecordError(ErrRequirePeerID) - return ErrRequirePeerID - } - - if len(e.host.Peerstore().Addrs(p.ID)) == 0 { - span.RecordError(endpoint.ErrUnknownPeer) - return endpoint.ErrUnknownPeer - } - - if responseHandlerFn == nil { - span.RecordError(endpoint.ErrNilResponseHandler) - return endpoint.ErrNilResponseHandler - } - - go func() { - ctx, span := util.StartSpan(e.ctx, - "Libp2pEndpoint.SendRequestHandleResponse libp2p go routine", - trace.WithAttributes( - attribute.String("PeerID", n.String()), - )) - defer span.End() - var cancel context.CancelFunc - if timeout > 0 { - ctx, cancel = e.sched.Clock().WithTimeout(ctx, timeout) - } else { - ctx, cancel = context.WithCancel(ctx) - } - defer cancel() - - var err error - - var s network.Stream - s, err = e.host.NewStream(ctx, p.ID, protocol.ID(protoID)) - if err != nil { - span.RecordError(err, trace.WithAttributes(attribute.String("where", "stream creation"))) - e.sched.EnqueueAction(ctx, event.BasicAction(func(ctx context.Context) { - responseHandlerFn(ctx, nil, err) - })) - return - } - defer s.Close() - - err = WriteMsg(s, protoReq) - if err != nil { - span.RecordError(err, trace.WithAttributes(attribute.String("where", "write message"))) - e.sched.EnqueueAction(ctx, event.BasicAction(func(ctx context.Context) { - responseHandlerFn(ctx, nil, err) - })) - return - } - - var timeoutEvent event.PlannedAction - // handle timeout - - if timeout != 0 { - timeoutEvent = event.ScheduleActionIn(ctx, e.sched, timeout, - event.BasicAction(func(ctx context.Context) { - cancel() - responseHandlerFn(ctx, nil, endpoint.ErrTimeout) - })) - } - - err = ReadMsg(s, protoResp) - if timeout != 0 { - // remove timeout if not too late - if !e.sched.RemovePlannedAction(ctx, timeoutEvent) { - span.RecordError(endpoint.ErrResponseReceivedAfterTimeout) - // don't run responseHandlerFn if timeout was already triggered - return - } - } - if err != nil { - span.RecordError(err, trace.WithAttributes(attribute.String("where", "read message"))) - e.sched.EnqueueAction(ctx, event.BasicAction(func(ctx context.Context) { - responseHandlerFn(ctx, protoResp, err) - })) - return - } - - span.AddEvent("response received") - e.sched.EnqueueAction(ctx, event.BasicAction(func(ctx context.Context) { - responseHandlerFn(ctx, protoResp, err) - })) - }() - return nil -} - -func (e *Libp2pEndpoint) Connectedness(id kad.NodeID[key.Key256]) (endpoint.Connectedness, error) { - p, err := getPeerID(id) - if err != nil { - return endpoint.NotConnected, err - } - - c := e.host.Network().Connectedness(p.ID) - switch c { - case network.NotConnected: - return endpoint.NotConnected, nil - case network.Connected: - return endpoint.Connected, nil - case network.CanConnect: - return endpoint.CanConnect, nil - case network.CannotConnect: - return endpoint.CannotConnect, nil - default: - panic(fmt.Sprintf("unexpected libp2p connectedness value: %v", c)) - } -} - -func (e *Libp2pEndpoint) PeerInfo(id kad.NodeID[key.Key256]) (peer.AddrInfo, error) { - p, err := getPeerID(id) - if err != nil { - return peer.AddrInfo{}, err - } - return e.host.Peerstore().PeerInfo(p.ID), nil -} - -func (e *Libp2pEndpoint) Key() key.Key256 { - return PeerID{ID: e.host.ID()}.Key() -} - -func (e *Libp2pEndpoint) NetworkAddress(n kad.NodeID[key.Key256]) (kad.NodeInfo[key.Key256, multiaddr.Multiaddr], error) { - ai, err := e.PeerInfo(n) - if err != nil { - return nil, err - } - return NewAddrInfo(ai), nil -} - -func (e *Libp2pEndpoint) AddRequestHandler(protoID address.ProtocolID, - req kad.Message, reqHandler endpoint.RequestHandlerFn[key.Key256], -) error { - protoReq, ok := req.(ProtoKadMessage) - if !ok { - return ErrRequireProtoKadMessage - } - if reqHandler == nil { - return endpoint.ErrNilRequestHandler - } - // when a new request comes in, we need to queue it - streamHandler := func(s network.Stream) { - e.sched.EnqueueAction(e.ctx, event.BasicAction(func(ctx context.Context) { - ctx, span := util.StartSpan(ctx, "Libp2pEndpoint.AddRequestHandler", - trace.WithAttributes( - attribute.String("PeerID", s.Conn().RemotePeer().String()), - )) - defer span.End() - defer s.Close() - - // create a protobuf reader and writer - r := pbio.NewDelimitedReader(s, network.MessageSizeMax) - w := pbio.NewDelimitedWriter(s) - - for { - // read a message from the stream - err := r.ReadMsg(protoReq) - if err != nil { - if err == io.EOF { - // stream EOF, all done - return - } - span.RecordError(err) - return - } - - requester := NewAddrInfo( - e.host.Peerstore().PeerInfo(s.Conn().RemotePeer()), - ) - resp, err := reqHandler(ctx, requester, req) - if err != nil { - span.RecordError(err) - return - } - - protoResp, ok := resp.(ProtoKadMessage) - if !ok { - err = errors.New("Libp2pEndpoint requires ProtoKadMessage") - span.RecordError(err) - return - } - - // write the response to the stream - err = w.WriteMsg(protoResp) - if err != nil { - span.RecordError(err) - return - } - } - })) - } - e.host.SetStreamHandler(protocol.ID(protoID), streamHandler) - return nil -} - -func (e *Libp2pEndpoint) RemoveRequestHandler(protoID address.ProtocolID) { - e.host.RemoveStreamHandler(protocol.ID(protoID)) -} diff --git a/libp2p/libp2pendpoint_test.go b/libp2p/libp2pendpoint_test.go deleted file mode 100644 index 6a06390..0000000 --- a/libp2p/libp2pendpoint_test.go +++ /dev/null @@ -1,558 +0,0 @@ -package libp2p - -import ( - "context" - "errors" - "sync" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/p2p/net/swarm" - ma "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr/net" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" - "github.com/plprobelab/go-kademlia/sim" -) - -var ( - peerstoreTTL = 10 * time.Minute - protoID = address.ProtocolID("/test/1.0.0") -) - -func createEndpoints(t *testing.T, ctx context.Context, nPeers int) ( - []*Libp2pEndpoint, []*AddrInfo, []*PeerID, - []event.AwareScheduler, -) { - clk := clock.New() - - scheds := make([]event.AwareScheduler, nPeers) - ids := make([]*PeerID, nPeers) - addrinfos := make([]*AddrInfo, nPeers) - endpoints := make([]*Libp2pEndpoint, nPeers) - for i := 0; i < nPeers; i++ { - scheds[i] = event.NewSimpleScheduler(clk) - host, err := libp2p.New() - require.NoError(t, err) - ids[i] = NewPeerID(host.ID()) - addrinfos[i] = NewAddrInfo(peer.AddrInfo{ - ID: ids[i].ID, - Addrs: host.Addrs(), - }) - endpoints[i] = NewLibp2pEndpoint(ctx, host, scheds[i]) - na, err := endpoints[i].NetworkAddress(ids[i]) - require.NoError(t, err) - for _, a := range na.Addresses() { - require.Contains(t, host.Addrs(), a) - } - } - return endpoints, addrinfos, ids, scheds -} - -func connectEndpoints(t *testing.T, ctx context.Context, endpoints []*Libp2pEndpoint, - addrInfos []*AddrInfo, -) { - require.Len(t, endpoints, len(addrInfos)) - for i, ep := range endpoints { - for j, ai := range addrInfos { - if i == j { - continue - } - err := ep.MaybeAddToPeerstore(ctx, ai, peerstoreTTL) - require.NoError(t, err) - } - } -} - -func TestConnections(t *testing.T) { - ctx := context.Background() - - // create endpoints - endpoints, addrs, ids, _ := createEndpoints(t, ctx, 4) - - invalidID := kadtest.NewInfo[key.Key256, ma.Multiaddr](kadtest.NewID(kadtest.NewStringID("invalid").Key()), nil) - - // test that the endpoint's kademlia key is as expected - for i, ep := range endpoints { - require.Equal(t, ids[i].Key(), ep.Key()) - } - - // add peer 1 to peer 0's peerstore - err := endpoints[0].MaybeAddToPeerstore(ctx, addrs[1], peerstoreTTL) - require.NoError(t, err) - // add invalid peer to peerstore - err = endpoints[0].MaybeAddToPeerstore(ctx, invalidID, peerstoreTTL) - require.Equal(t, endpoint.ErrInvalidPeer, err) - // should add no information for self - err = endpoints[0].MaybeAddToPeerstore(ctx, addrs[0], peerstoreTTL) - require.NoError(t, err) - // add peer 1 to peer 0's peerstore - err = endpoints[0].MaybeAddToPeerstore(ctx, addrs[3], peerstoreTTL) - require.NoError(t, err) - - // test connectedness, not connected but known address -> NotConnected - connectedness, err := endpoints[0].Connectedness(ids[1]) - require.NoError(t, err) - require.Equal(t, endpoint.NotConnected, connectedness) - // not connected, unknown address -> NotConnected - connectedness, err = endpoints[0].Connectedness(ids[2]) - require.NoError(t, err) - require.Equal(t, endpoint.NotConnected, connectedness) - // invalid peerid -> error - connectedness, err = endpoints[1].Connectedness(invalidID.ID()) - require.Equal(t, endpoint.ErrInvalidPeer, err) - require.Equal(t, endpoint.NotConnected, connectedness) - // verify peerinfo for invalid peerid - peerinfo, err := endpoints[1].PeerInfo(invalidID.ID()) - require.Equal(t, endpoint.ErrInvalidPeer, err) - require.Len(t, peerinfo.Addrs, 0) - // verify network address for valid peerid - netAddr, err := endpoints[0].NetworkAddress(ids[1]) - require.NoError(t, err) - ai, ok := netAddr.(*AddrInfo) - require.True(t, ok) - require.Equal(t, ids[1].ID, ai.PeerID().ID) - require.Len(t, ai.Addrs, len(addrs[1].Addrs)) - for _, addr := range ai.Addrs { - require.Contains(t, addrs[1].Addrs, addr) - } - // verify network address for invalid peerid - netAddr, err = endpoints[0].NetworkAddress(invalidID.ID()) - require.Equal(t, endpoint.ErrInvalidPeer, err) - require.Nil(t, netAddr) - // dial from 0 to 1 - err = endpoints[0].DialPeer(ctx, ids[1]) - require.NoError(t, err) - // test connectedness - connectedness, err = endpoints[0].Connectedness(ids[1]) - require.NoError(t, err) - require.Equal(t, endpoint.Connected, connectedness) - - // test peerinfo - peerinfo, err = endpoints[0].PeerInfo(ids[1]) - require.NoError(t, err) - // filter out loopback addresses - expectedAddrs := ma.FilterAddrs(addrs[1].Addrs, func(a ma.Multiaddr) bool { - return !manet.IsIPLoopback(a) - }) - for _, addr := range expectedAddrs { - require.Contains(t, peerinfo.Addrs, addr, addr.String(), expectedAddrs) - } - peerinfo, err = endpoints[0].PeerInfo(ids[2]) - require.NoError(t, err) - require.Len(t, peerinfo.Addrs, 0) - - // dial from 1 to invalid peerid - err = endpoints[1].DialPeer(ctx, invalidID.ID()) - require.Error(t, err) - require.Equal(t, endpoint.ErrInvalidPeer, err) - // dial again from 0 to 1, no is already connected - err = endpoints[0].DialPeer(ctx, ids[1]) - require.NoError(t, err) - // dial from 1 to 0, works because 1 is already connected to 0 - err = endpoints[1].DialPeer(ctx, ids[0]) - require.NoError(t, err) - // dial from 0 to 2, fails because 0 doesn't know 2's addresses - err = endpoints[0].DialPeer(ctx, ids[2]) - require.Error(t, err) -} - -func TestAsyncDial(t *testing.T) { - ctx := context.Background() - wg := sync.WaitGroup{} - - endpoints, addrs, ids, scheds := createEndpoints(t, ctx, 4) - - // test async dial and report from 0 to 1 - err := endpoints[0].MaybeAddToPeerstore(ctx, addrs[1], peerstoreTTL) - require.NoError(t, err) - wg.Add(1) - err = endpoints[0].AsyncDialAndReport(ctx, ids[1], func(ctx context.Context, success bool) { - require.True(t, success) - wg.Done() - }) - require.NoError(t, err) - wg.Add(1) - go func() { - // AsyncDialAndReport adds the dial action to the event queue, so we - // need to run the scheduler - for !scheds[0].RunOne(ctx) { - scheds[0].Clock().Sleep(time.Millisecond) - } - wg.Done() - }() - wg.Wait() - // nothing to run for both schedulers - for _, s := range scheds { - require.Equal(t, event.MaxTime, s.NextActionTime(ctx)) - } - - // test async dial and report from 0 to 2 (already connected) - err = endpoints[0].MaybeAddToPeerstore(ctx, addrs[2], peerstoreTTL) - require.NoError(t, err) - err = endpoints[0].DialPeer(ctx, ids[2]) - require.NoError(t, err) - err = endpoints[0].AsyncDialAndReport(ctx, ids[1], func(ctx context.Context, success bool) { - require.True(t, success) - }) - require.NoError(t, err) - // nothing to run for both schedulers - for _, s := range scheds { - require.Equal(t, event.MaxTime, s.NextActionTime(ctx)) - } - - // test async dial and report from 0 to 3 (unknown address) - wg.Add(1) - endpoints[0].AsyncDialAndReport(ctx, ids[3], func(ctx context.Context, success bool) { - require.False(t, success) - wg.Done() - }) - wg.Add(1) - go func() { - // AsyncDialAndReport adds the dial action to the event queue, so we - // need to run the scheduler - for !scheds[0].RunOne(ctx) { - scheds[0].Clock().Sleep(time.Millisecond) - } - wg.Done() - }() - wg.Wait() - // nothing to run for both schedulers - for _, s := range scheds { - require.Equal(t, s.NextActionTime(ctx), event.MaxTime) - } - - // test asyc dial with invalid peerid - err = endpoints[0].AsyncDialAndReport(ctx, kadtest.NewStringID("invalid"), nil) - require.Equal(t, endpoint.ErrInvalidPeer, err) - // nothing to run for both schedulers - for _, s := range scheds { - require.Equal(t, event.MaxTime, s.NextActionTime(ctx)) - } -} - -func TestRequestHandler(t *testing.T) { - ctx := context.Background() - endpoints, _, _, _ := createEndpoints(t, ctx, 1) - - // set node 1 to server mode - requestHandler := func(ctx context.Context, id kad.NodeID[key.Key256], - req kad.Message, - ) (kad.Message, error) { - // request handler returning the received message - return req, nil - } - err := endpoints[0].AddRequestHandler(protoID, &Message{}, requestHandler) - require.NoError(t, err) - err = endpoints[0].AddRequestHandler(protoID, &Message{}, nil) - require.Equal(t, endpoint.ErrNilRequestHandler, err) - - // invalid message format for handler - err = endpoints[0].AddRequestHandler("/fail/1.0.0", &sim.Message[key.Key256, ma.Multiaddr]{}, requestHandler) - require.Equal(t, ErrRequireProtoKadMessage, err) - - // remove request handler - endpoints[0].RemoveRequestHandler(protoID) -} - -func TestReqFailFast(t *testing.T) { - ctx := context.Background() - - endpoints, addrs, ids, _ := createEndpoints(t, ctx, 3) - connectEndpoints(t, ctx, endpoints[:2], addrs[:2]) - - req := FindPeerRequest(ids[1]) - // invalid response format (not protobuf) - err := endpoints[0].SendRequestHandleResponse(ctx, protoID, ids[1], req, - &sim.Message[key.Key256, ma.Multiaddr]{}, time.Second, nil) - require.Equal(t, ErrRequireProtoKadResponse, err) - - // invalid request format (not protobuf) - err = endpoints[0].SendRequestHandleResponse(ctx, protoID, ids[1], - &sim.Message[key.Key256, ma.Multiaddr]{}, &Message{}, time.Second, nil) - require.Equal(t, ErrRequireProtoKadMessage, err) - - // invalid recipient (not a peerid) - err = endpoints[0].SendRequestHandleResponse(ctx, protoID, - kadtest.NewStringID("invalid"), req, &Message{}, time.Second, nil) - require.Equal(t, ErrRequirePeerID, err) - - // nil response handler - err = endpoints[0].SendRequestHandleResponse(ctx, protoID, ids[1], req, - &Message{}, time.Second, nil) - require.Equal(t, endpoint.ErrNilResponseHandler, err) - - // non nil response handler that should not be called - responseHandler := func(_ context.Context, _ kad.Response[key.Key256, ma.Multiaddr], err error) { - require.Fail(t, "response handler shouldn't be called") - } - - // peer 0 isn't connected to peer 2, so it should fail fast - err = endpoints[0].SendRequestHandleResponse(ctx, protoID, ids[2], req, - &Message{}, time.Second, responseHandler) - require.Equal(t, endpoint.ErrUnknownPeer, err) -} - -func TestSuccessfulRequest(t *testing.T) { - ctx := context.Background() - - endpoints, addrs, ids, scheds := createEndpoints(t, ctx, 2) - connectEndpoints(t, ctx, endpoints, addrs) - - // set node 1 to server mode - requestHandler := func(ctx context.Context, id kad.NodeID[key.Key256], - req kad.Message, - ) (kad.Message, error) { - // request handler returning the received message - return req, nil - } - err := endpoints[1].AddRequestHandler(protoID, &Message{}, requestHandler) - require.NoError(t, err) - - wg := sync.WaitGroup{} - responseHandler := func(ctx context.Context, - resp kad.Response[key.Key256, ma.Multiaddr], err error, - ) { - wg.Done() - } - req := FindPeerRequest(ids[1]) - wg.Add(1) - endpoints[0].SendRequestHandleResponse(ctx, protoID, ids[1], req, - &Message{}, time.Second, responseHandler) - wg.Add(2) - go func() { - // run server 1 - for !scheds[1].RunOne(ctx) { - scheds[1].Clock().Sleep(time.Millisecond) - } - require.False(t, scheds[1].RunOne(ctx)) // only 1 action should run on server - wg.Done() - }() - go func() { - // timeout is queued in the scheduler 0 - for !scheds[0].RunOne(ctx) { - scheds[0].Clock().Sleep(time.Millisecond) - } - require.False(t, scheds[0].RunOne(ctx)) - wg.Done() - }() - wg.Wait() - // nothing to run for both schedulers - for _, s := range scheds { - require.Equal(t, event.MaxTime, s.NextActionTime(ctx)) - } -} - -func TestReqUnknownPeer(t *testing.T) { - ctx := context.Background() - - // create endpoints - endpoints, addrs, ids, scheds := createEndpoints(t, ctx, 2) - // replace address of peer 1 with an invalid address - addrs[1] = NewAddrInfo(peer.AddrInfo{ - ID: addrs[1].PeerID().ID, - Addrs: []ma.Multiaddr{ma.StringCast("/ip4/1.2.3.4")}, - }) - // "connect" the endpoints. 0 will store an invalid address for 1, so it - // won't fail fast, because it believes that it knows how to reach 1 - connectEndpoints(t, ctx, endpoints, addrs) - - // verfiy that the wrong address is stored for peer 1 in peer 0 - na, err := endpoints[0].NetworkAddress(ids[1]) - require.NoError(t, err) - for _, addr := range na.Addresses() { - require.Contains(t, addrs[1].Addrs, addr) - } - - req := FindPeerRequest(ids[1]) - done := make(chan struct{}) - - responseHandler := func(_ context.Context, _ kad.Response[key.Key256, ma.Multiaddr], err error) { - defer close(done) - require.ErrorIs(t, err, swarm.ErrNoGoodAddresses) - } - - // unknown valid peerid (address not stored in peerstore) - err = endpoints[0].SendRequestHandleResponse(ctx, protoID, ids[1], req, - &Message{}, time.Second, responseHandler) - require.NoError(t, err) - go func() { - // timeout is queued in the scheduler 0 - for !scheds[0].RunOne(ctx) { - scheds[0].Clock().Sleep(time.Millisecond) - } - require.False(t, scheds[0].RunOne(ctx)) - }() - - select { - case <-done: - case <-time.After(5 * time.Second): - t.Fatal("timeout") - } - - // nothing to run for both schedulers - for _, s := range scheds { - require.Equal(t, event.MaxTime, s.NextActionTime(ctx)) - } -} - -func TestReqTimeout(t *testing.T) { - ctx := context.Background() - - endpoints, addrs, ids, scheds := createEndpoints(t, ctx, 2) - connectEndpoints(t, ctx, endpoints, addrs) - - req := FindPeerRequest(ids[1]) - - wg := sync.WaitGroup{} - - err := endpoints[1].AddRequestHandler(protoID, &Message{}, func(ctx context.Context, - id kad.NodeID[key.Key256], req kad.Message, - ) (kad.Message, error) { - return req, nil - }) - require.NoError(t, err) - - responseHandler := func(_ context.Context, _ kad.Response[key.Key256, ma.Multiaddr], err error) { - require.Error(t, err) - wg.Done() - } - // timeout after 100 ms, will fail immediately - err = endpoints[0].SendRequestHandleResponse(ctx, protoID, ids[1], req, - &Message{}, 100*time.Millisecond, responseHandler) - require.NoError(t, err) - wg.Add(2) - go func() { - // timeout is queued in the scheduler 0 - for !scheds[0].RunOne(ctx) { - scheds[0].Clock().Sleep(time.Millisecond) - } - require.False(t, scheds[0].RunOne(ctx)) - wg.Done() - }() - wg.Wait() - // now that the client has timed out, the server will send back the reply - require.True(t, scheds[1].RunOne(ctx)) - - // nothing to run for both schedulers - for _, s := range scheds { - require.Equal(t, event.MaxTime, s.NextActionTime(ctx)) - } -} - -func TestReqHandlerError(t *testing.T) { - // server request handler error - ctx := context.Background() - - endpoints, addrs, ids, scheds := createEndpoints(t, ctx, 2) - connectEndpoints(t, ctx, endpoints, addrs) - - req := FindPeerRequest(ids[1]) - - wg := sync.WaitGroup{} - - err := endpoints[1].AddRequestHandler(protoID, &Message{}, func(ctx context.Context, - id kad.NodeID[key.Key256], req kad.Message, - ) (kad.Message, error) { - // request handler returns error - return nil, errors.New("server error") - }) - require.NoError(t, err) - // responseHandler is run after context is cancelled - responseHandler := func(ctx context.Context, - resp kad.Response[key.Key256, ma.Multiaddr], err error, - ) { - wg.Done() - require.Error(t, err) - } - noResponseCtx, cancel := context.WithCancel(ctx) - wg.Add(1) - err = endpoints[0].SendRequestHandleResponse(noResponseCtx, protoID, ids[1], req, - &Message{}, 0, responseHandler) - require.NoError(t, err) - wg.Add(2) - go func() { - for !scheds[1].RunOne(ctx) { - scheds[1].Clock().Sleep(time.Millisecond) - } - require.False(t, scheds[1].RunOne(ctx)) - cancel() - wg.Done() - }() - go func() { - // timeout is queued in the scheduler 0 - for !scheds[0].RunOne(ctx) { - scheds[0].Clock().Sleep(time.Millisecond) - } - require.False(t, scheds[0].RunOne(ctx)) - wg.Done() - }() - wg.Wait() - // nothing to run for both schedulers - for _, s := range scheds { - require.Equal(t, event.MaxTime, s.NextActionTime(ctx)) - } -} - -func TestReqHandlerReturnsWrongType(t *testing.T) { - // server request handler returns wrong message type, no response is - // returned by the server - ctx := context.Background() - - endpoints, addrs, ids, scheds := createEndpoints(t, ctx, 2) - connectEndpoints(t, ctx, endpoints, addrs) - - req := FindPeerRequest(ids[1]) - - wg := sync.WaitGroup{} - err := endpoints[1].AddRequestHandler(protoID, &Message{}, func(ctx context.Context, - id kad.NodeID[key.Key256], req kad.Message, - ) (kad.Message, error) { - // request handler returns error - return &sim.Message[key.Key256, ma.Multiaddr]{}, nil - }) - require.NoError(t, err) - // responseHandler is run after context is cancelled - responseHandler := func(ctx context.Context, - resp kad.Response[key.Key256, ma.Multiaddr], err error, - ) { - wg.Done() - require.Error(t, err) - } - noResponseCtx, cancel := context.WithCancel(ctx) - wg.Add(1) - err = endpoints[0].SendRequestHandleResponse(noResponseCtx, protoID, ids[1], req, - &Message{}, 0, responseHandler) - require.NoError(t, err) - wg.Add(2) - go func() { - for !scheds[1].RunOne(ctx) { - scheds[1].Clock().Sleep(time.Millisecond) - } - require.False(t, scheds[1].RunOne(ctx)) - cancel() - wg.Done() - }() - go func() { - // timeout is queued in the scheduler 0 - for !scheds[0].RunOne(ctx) { - scheds[0].Clock().Sleep(time.Millisecond) - } - require.False(t, scheds[0].RunOne(ctx)) - wg.Done() - }() - wg.Wait() - // nothing to run for both schedulers - for _, s := range scheds { - require.Equal(t, event.MaxTime, s.NextActionTime(ctx)) - } -} diff --git a/libp2p/message.pb.go b/libp2p/message.pb.go deleted file mode 100644 index 109645b..0000000 --- a/libp2p/message.pb.go +++ /dev/null @@ -1,545 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.29.1 -// protoc v3.21.12 -// source: message.proto - -package libp2p - -import ( - reflect "reflect" - sync "sync" - - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Message_MessageType int32 - -const ( - Message_PUT_VALUE Message_MessageType = 0 - Message_GET_VALUE Message_MessageType = 1 - Message_ADD_PROVIDER Message_MessageType = 2 - Message_GET_PROVIDERS Message_MessageType = 3 - Message_FIND_NODE Message_MessageType = 4 - Message_PING Message_MessageType = 5 -) - -// Enum value maps for Message_MessageType. -var ( - Message_MessageType_name = map[int32]string{ - 0: "PUT_VALUE", - 1: "GET_VALUE", - 2: "ADD_PROVIDER", - 3: "GET_PROVIDERS", - 4: "FIND_NODE", - 5: "PING", - } - Message_MessageType_value = map[string]int32{ - "PUT_VALUE": 0, - "GET_VALUE": 1, - "ADD_PROVIDER": 2, - "GET_PROVIDERS": 3, - "FIND_NODE": 4, - "PING": 5, - } -) - -func (x Message_MessageType) Enum() *Message_MessageType { - p := new(Message_MessageType) - *p = x - return p -} - -func (x Message_MessageType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Message_MessageType) Descriptor() protoreflect.EnumDescriptor { - return file_message_proto_enumTypes[0].Descriptor() -} - -func (Message_MessageType) Type() protoreflect.EnumType { - return &file_message_proto_enumTypes[0] -} - -func (x Message_MessageType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Message_MessageType.Descriptor instead. -func (Message_MessageType) EnumDescriptor() ([]byte, []int) { - return file_message_proto_rawDescGZIP(), []int{1, 0} -} - -type Message_ConnectionType int32 - -const ( - // sender does not have a connection to peer, and no extra information - // (default) - Message_NOT_CONNECTED Message_ConnectionType = 0 - // sender has a live connection to peer - Message_CONNECTED Message_ConnectionType = 1 - // sender recently connected to peer - Message_CAN_CONNECT Message_ConnectionType = 2 - // sender recently tried to connect to peer repeatedly but failed to connect - // ("try" here is loose, but this should signal "made strong effort, - // failed") - Message_CANNOT_CONNECT Message_ConnectionType = 3 -) - -// Enum value maps for Message_ConnectionType. -var ( - Message_ConnectionType_name = map[int32]string{ - 0: "NOT_CONNECTED", - 1: "CONNECTED", - 2: "CAN_CONNECT", - 3: "CANNOT_CONNECT", - } - Message_ConnectionType_value = map[string]int32{ - "NOT_CONNECTED": 0, - "CONNECTED": 1, - "CAN_CONNECT": 2, - "CANNOT_CONNECT": 3, - } -) - -func (x Message_ConnectionType) Enum() *Message_ConnectionType { - p := new(Message_ConnectionType) - *p = x - return p -} - -func (x Message_ConnectionType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Message_ConnectionType) Descriptor() protoreflect.EnumDescriptor { - return file_message_proto_enumTypes[1].Descriptor() -} - -func (Message_ConnectionType) Type() protoreflect.EnumType { - return &file_message_proto_enumTypes[1] -} - -func (x Message_ConnectionType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Message_ConnectionType.Descriptor instead. -func (Message_ConnectionType) EnumDescriptor() ([]byte, []int) { - return file_message_proto_rawDescGZIP(), []int{1, 1} -} - -// Record represents a dht record that contains a value -// for a key value pair -type Record struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The key that references this record - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - // The actual value this record is storing - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - // Time the record was received, set by receiver - TimeReceived string `protobuf:"bytes,5,opt,name=timeReceived,proto3" json:"timeReceived,omitempty"` - // The original publisher of the record - // Not used at the moment, but may be useful for future developments - Publisher []byte `protobuf:"bytes,666,opt,name=publisher,proto3" json:"publisher,omitempty"` - // The remaining TTL of the record, in seconds - // Not used at the moment, but may be useful for future developments - Ttl uint32 `protobuf:"varint,777,opt,name=ttl,proto3" json:"ttl,omitempty"` -} - -func (x *Record) Reset() { - *x = Record{} - if protoimpl.UnsafeEnabled { - mi := &file_message_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Record) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Record) ProtoMessage() {} - -func (x *Record) ProtoReflect() protoreflect.Message { - mi := &file_message_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Record.ProtoReflect.Descriptor instead. -func (*Record) Descriptor() ([]byte, []int) { - return file_message_proto_rawDescGZIP(), []int{0} -} - -func (x *Record) GetKey() []byte { - if x != nil { - return x.Key - } - return nil -} - -func (x *Record) GetValue() []byte { - if x != nil { - return x.Value - } - return nil -} - -func (x *Record) GetTimeReceived() string { - if x != nil { - return x.TimeReceived - } - return "" -} - -func (x *Record) GetPublisher() []byte { - if x != nil { - return x.Publisher - } - return nil -} - -func (x *Record) GetTtl() uint32 { - if x != nil { - return x.Ttl - } - return 0 -} - -type Message struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // defines what type of message it is - Type Message_MessageType `protobuf:"varint,1,opt,name=type,proto3,enum=dht.pb.Message_MessageType" json:"type,omitempty"` - // defines what coral cluster level this query/response belongs to - // in case we want to implement coral's cluster rings in the future - ClusterLevelRaw int32 `protobuf:"varint,10,opt,name=clusterLevelRaw,proto3" json:"clusterLevelRaw,omitempty"` // NOT USED - // Used to specify the key associated with this message - // PUT_VALUE, GET_VALUE, ADD_PROVIDER, GET_PROVIDERS - Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` - // Used to return a value - // PUT_VALUE, GET_VALUE - Record *Record `protobuf:"bytes,3,opt,name=record,proto3" json:"record,omitempty"` - // Used to return peers closer to a key in a query - // GET_VALUE, GET_PROVIDERS, FIND_NODE - CloserPeers []*Message_Peer `protobuf:"bytes,8,rep,name=closerPeers,proto3" json:"closerPeers,omitempty"` - // Used to return Providers - // GET_VALUE, ADD_PROVIDER, GET_PROVIDERS - ProviderPeers []*Message_Peer `protobuf:"bytes,9,rep,name=providerPeers,proto3" json:"providerPeers,omitempty"` -} - -func (x *Message) Reset() { - *x = Message{} - if protoimpl.UnsafeEnabled { - mi := &file_message_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Message) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Message) ProtoMessage() {} - -func (x *Message) ProtoReflect() protoreflect.Message { - mi := &file_message_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Message.ProtoReflect.Descriptor instead. -func (*Message) Descriptor() ([]byte, []int) { - return file_message_proto_rawDescGZIP(), []int{1} -} - -func (x *Message) GetType() Message_MessageType { - if x != nil { - return x.Type - } - return Message_PUT_VALUE -} - -func (x *Message) GetClusterLevelRaw() int32 { - if x != nil { - return x.ClusterLevelRaw - } - return 0 -} - -func (x *Message) GetKey() []byte { - if x != nil { - return x.Key - } - return nil -} - -func (x *Message) GetRecord() *Record { - if x != nil { - return x.Record - } - return nil -} - -func (x *Message) GetCloserPeers() []*Message_Peer { - if x != nil { - return x.CloserPeers - } - return nil -} - -func (x *Message) GetProviderPeers() []*Message_Peer { - if x != nil { - return x.ProviderPeers - } - return nil -} - -type Message_Peer struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // ID of a given peer. - Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // multiaddrs for a given peer - Addrs [][]byte `protobuf:"bytes,2,rep,name=addrs,proto3" json:"addrs,omitempty"` - // used to signal the sender's connection capabilities to the peer - Connection Message_ConnectionType `protobuf:"varint,3,opt,name=connection,proto3,enum=dht.pb.Message_ConnectionType" json:"connection,omitempty"` -} - -func (x *Message_Peer) Reset() { - *x = Message_Peer{} - if protoimpl.UnsafeEnabled { - mi := &file_message_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Message_Peer) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Message_Peer) ProtoMessage() {} - -func (x *Message_Peer) ProtoReflect() protoreflect.Message { - mi := &file_message_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Message_Peer.ProtoReflect.Descriptor instead. -func (*Message_Peer) Descriptor() ([]byte, []int) { - return file_message_proto_rawDescGZIP(), []int{1, 0} -} - -func (x *Message_Peer) GetId() []byte { - if x != nil { - return x.Id - } - return nil -} - -func (x *Message_Peer) GetAddrs() [][]byte { - if x != nil { - return x.Addrs - } - return nil -} - -func (x *Message_Peer) GetConnection() Message_ConnectionType { - if x != nil { - return x.Connection - } - return Message_NOT_CONNECTED -} - -var File_message_proto protoreflect.FileDescriptor - -var file_message_proto_rawDesc = []byte{ - 0x0a, 0x0d, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, - 0x06, 0x64, 0x68, 0x74, 0x2e, 0x70, 0x62, 0x22, 0x86, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x69, - 0x6d, 0x65, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x12, 0x1d, - 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x18, 0x9a, 0x05, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x12, 0x11, 0x0a, - 0x03, 0x74, 0x74, 0x6c, 0x18, 0x89, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x74, 0x6c, - 0x22, 0xc4, 0x04, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2f, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x64, 0x68, 0x74, - 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, - 0x0f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x61, 0x77, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x52, 0x61, 0x77, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x06, 0x72, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x68, 0x74, 0x2e, - 0x70, 0x62, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x06, 0x72, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x12, 0x36, 0x0a, 0x0b, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x72, 0x50, 0x65, 0x65, 0x72, 0x73, - 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x68, 0x74, 0x2e, 0x70, 0x62, 0x2e, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x0b, 0x63, 0x6c, - 0x6f, 0x73, 0x65, 0x72, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3a, 0x0a, 0x0d, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x64, 0x68, 0x74, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x50, 0x65, 0x65, 0x72, 0x73, 0x1a, 0x6c, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, - 0x05, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x61, 0x64, - 0x64, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x64, 0x68, 0x74, 0x2e, 0x70, 0x62, - 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x69, 0x0a, 0x0b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x55, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, - 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x47, 0x45, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x01, - 0x12, 0x10, 0x0a, 0x0c, 0x41, 0x44, 0x44, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, - 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x45, 0x54, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, - 0x45, 0x52, 0x53, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x49, 0x4e, 0x44, 0x5f, 0x4e, 0x4f, - 0x44, 0x45, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x22, 0x57, - 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x11, 0x0a, 0x0d, 0x4e, 0x4f, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x45, 0x44, - 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x41, 0x4e, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, - 0x54, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x43, 0x4f, - 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x03, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x2e, 0x2f, 0x69, 0x70, - 0x66, 0x73, 0x6b, 0x61, 0x64, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_message_proto_rawDescOnce sync.Once - file_message_proto_rawDescData = file_message_proto_rawDesc -) - -func file_message_proto_rawDescGZIP() []byte { - file_message_proto_rawDescOnce.Do(func() { - file_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_message_proto_rawDescData) - }) - return file_message_proto_rawDescData -} - -var ( - file_message_proto_enumTypes = make([]protoimpl.EnumInfo, 2) - file_message_proto_msgTypes = make([]protoimpl.MessageInfo, 3) - file_message_proto_goTypes = []interface{}{ - (Message_MessageType)(0), // 0: dht.pb.Message.MessageType - (Message_ConnectionType)(0), // 1: dht.pb.Message.ConnectionType - (*Record)(nil), // 2: dht.pb.Record - (*Message)(nil), // 3: dht.pb.Message - (*Message_Peer)(nil), // 4: dht.pb.Message.Peer - } -) - -var file_message_proto_depIdxs = []int32{ - 0, // 0: dht.pb.Message.type:type_name -> dht.pb.Message.MessageType - 2, // 1: dht.pb.Message.record:type_name -> dht.pb.Record - 4, // 2: dht.pb.Message.closerPeers:type_name -> dht.pb.Message.Peer - 4, // 3: dht.pb.Message.providerPeers:type_name -> dht.pb.Message.Peer - 1, // 4: dht.pb.Message.Peer.connection:type_name -> dht.pb.Message.ConnectionType - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name -} - -func init() { file_message_proto_init() } -func file_message_proto_init() { - if File_message_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Record); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_message_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Message); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_message_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Message_Peer); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_message_proto_rawDesc, - NumEnums: 2, - NumMessages: 3, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_message_proto_goTypes, - DependencyIndexes: file_message_proto_depIdxs, - EnumInfos: file_message_proto_enumTypes, - MessageInfos: file_message_proto_msgTypes, - }.Build() - File_message_proto = out.File - file_message_proto_rawDesc = nil - file_message_proto_goTypes = nil - file_message_proto_depIdxs = nil -} diff --git a/libp2p/message.proto b/libp2p/message.proto deleted file mode 100644 index 0fd77dd..0000000 --- a/libp2p/message.proto +++ /dev/null @@ -1,93 +0,0 @@ -syntax = "proto3"; -package dht.pb; - -option go_package = "../ipfsv1"; - -// Record represents a dht record that contains a value -// for a key value pair -message Record { - // The key that references this record - bytes key = 1; - - // The actual value this record is storing - bytes value = 2; - - // Note: These fields were removed from the Record message - // hash of the authors public key - // optional string author = 3; - // A PKI signature for the key+value+author - // optional bytes signature = 4; - - // Time the record was received, set by receiver - string timeReceived = 5; - - // The original publisher of the record - // Not used at the moment, but may be useful for future developments - bytes publisher = 666; - - // The remaining TTL of the record, in seconds - // Not used at the moment, but may be useful for future developments - uint32 ttl = 777; -}; - -message Message { - enum MessageType { - PUT_VALUE = 0; - GET_VALUE = 1; - ADD_PROVIDER = 2; - GET_PROVIDERS = 3; - FIND_NODE = 4; - PING = 5; - } - - enum ConnectionType { - // sender does not have a connection to peer, and no extra information - // (default) - NOT_CONNECTED = 0; - - // sender has a live connection to peer - CONNECTED = 1; - - // sender recently connected to peer - CAN_CONNECT = 2; - - // sender recently tried to connect to peer repeatedly but failed to connect - // ("try" here is loose, but this should signal "made strong effort, - // failed") - CANNOT_CONNECT = 3; - } - - message Peer { - // ID of a given peer. - bytes id = 1; - - // multiaddrs for a given peer - repeated bytes addrs = 2; - - // used to signal the sender's connection capabilities to the peer - ConnectionType connection = 3; - } - - // defines what type of message it is - MessageType type = 1; - - // defines what coral cluster level this query/response belongs to - // in case we want to implement coral's cluster rings in the future - int32 clusterLevelRaw = 10; // NOT USED - - // Used to specify the key associated with this message - // PUT_VALUE, GET_VALUE, ADD_PROVIDER, GET_PROVIDERS - bytes key = 2; - - // Used to return a value - // PUT_VALUE, GET_VALUE - Record record = 3; - - // Used to return peers closer to a key in a query - // GET_VALUE, GET_PROVIDERS, FIND_NODE - repeated Peer closerPeers = 8; - - // Used to return Providers - // GET_VALUE, ADD_PROVIDER, GET_PROVIDERS - repeated Peer providerPeers = 9; -} \ No newline at end of file diff --git a/libp2p/peerid.go b/libp2p/peerid.go deleted file mode 100644 index 59f9179..0000000 --- a/libp2p/peerid.go +++ /dev/null @@ -1,30 +0,0 @@ -package libp2p - -import ( - "github.com/libp2p/go-libp2p/core/peer" - mh "github.com/multiformats/go-multihash" - mhreg "github.com/multiformats/go-multihash/core" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" -) - -type PeerID struct { - peer.ID -} - -var _ kad.NodeID[key.Key256] = (*PeerID)(nil) - -func NewPeerID(p peer.ID) *PeerID { - return &PeerID{p} -} - -func (id PeerID) Key() key.Key256 { - hasher, _ := mhreg.GetHasher(mh.SHA2_256) - hasher.Write([]byte(id.ID)) - return key.NewKey256(hasher.Sum(nil)) -} - -func (id PeerID) NodeID() kad.NodeID[key.Key256] { - return &id -} diff --git a/libp2p/proto_msg.go b/libp2p/proto_msg.go deleted file mode 100644 index 877b8c7..0000000 --- a/libp2p/proto_msg.go +++ /dev/null @@ -1,21 +0,0 @@ -package libp2p - -import ( - "google.golang.org/protobuf/proto" - - "github.com/plprobelab/go-kademlia/kad" -) - -type ProtoKadMessage interface { - proto.Message -} - -type ProtoKadRequestMessage[K kad.Key[K], A kad.Address[A]] interface { - ProtoKadMessage - kad.Request[K, A] -} - -type ProtoKadResponseMessage[K kad.Key[K], A kad.Address[A]] interface { - ProtoKadMessage - kad.Response[K, A] -} diff --git a/libp2p/query_test.go b/libp2p/query_test.go deleted file mode 100644 index 4c2e77a..0000000 --- a/libp2p/query_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package libp2p - -import ( - "context" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/query/simplequery" - "github.com/plprobelab/go-kademlia/routing/simplert" - "github.com/plprobelab/go-kademlia/sim" -) - -// TestLibp2pCornerCase tests that the newRequest(ctx) can fail fast if the -// selected peer has an invalid format (e.g for libp2p something that isn't a -// peerid.PeerID). This test should be performed using the libp2p endpoint -// because the fakeendpoint can never fail fast. -func TestLibp2pCornerCase(t *testing.T) { - t.Skip() - - ctx := context.Background() - clk := clock.New() - - protoID := address.ProtocolID("/test/1.0.0") - bucketSize := 1 - peerstoreTTL := time.Minute - - h, err := libp2p.New() - require.NoError(t, err) - id := NewPeerID(h.ID()) - sched := event.NewSimpleScheduler(clk) - libp2pEndpoint := NewLibp2pEndpoint(ctx, h, sched) - rt := simplert.New[key.Key256, kad.NodeID[key.Key256]](id, bucketSize) - - parsed, err := peer.Decode("1D3oooUnknownPeer") - require.NoError(t, err) - addrInfo := NewAddrInfo(peer.AddrInfo{ - ID: parsed, - Addrs: []multiaddr.Multiaddr{multiaddr.StringCast("/ip4/1.1.1.1/tcp/1")}, - }) - success := rt.AddNode(addrInfo.ID()) - require.True(t, success) - err = libp2pEndpoint.MaybeAddToPeerstore(ctx, addrInfo, peerstoreTTL) - require.NoError(t, err) - - queryOpts := []simplequery.Option[key.Key256, multiaddr.Multiaddr]{ - simplequery.WithProtocolID[key.Key256, multiaddr.Multiaddr](protoID), - simplequery.WithConcurrency[key.Key256, multiaddr.Multiaddr](1), - simplequery.WithNumberUsefulCloserPeers[key.Key256, multiaddr.Multiaddr](bucketSize), - simplequery.WithRequestTimeout[key.Key256, multiaddr.Multiaddr](time.Millisecond), - simplequery.WithEndpoint[key.Key256, multiaddr.Multiaddr](libp2pEndpoint), - simplequery.WithRoutingTable[key.Key256, multiaddr.Multiaddr](rt), - simplequery.WithScheduler[key.Key256, multiaddr.Multiaddr](sched), - } - - req := sim.NewRequest[key.Key256, multiaddr.Multiaddr](kadtest.NewStringID("RandomKey").Key()) - - q2, err := simplequery.NewSimpleQuery[key.Key256, multiaddr.Multiaddr](ctx, NewPeerID(h.ID()), req, queryOpts...) - require.NoError(t, err) - - // TODO: moved this test here from simplequery to remove libp2p dependency from there - // this means we can't set the peerlist endpoint to nil... - // skipping this test for now. - _ = q2 - //// set the peerlist endpoint to nil, to allow invalid NodeIDs in peerlist - //q2.peerlist.endpoint = nil - //// change the node id of the queued peer. sending the message with - //// SendRequestHandleResponse will fail fast (no new go routine created) - //q2.peerlist.closest.id = kadtest.NewID(key.ZeroKey256()) - - require.True(t, sched.RunOne(ctx)) - require.False(t, sched.RunOne(ctx)) -} diff --git a/libp2p/util.go b/libp2p/util.go deleted file mode 100644 index 506260f..0000000 --- a/libp2p/util.go +++ /dev/null @@ -1,17 +0,0 @@ -package libp2p - -import ( - "github.com/libp2p/go-libp2p/core/network" - "github.com/libp2p/go-msgio/pbio" - "google.golang.org/protobuf/reflect/protoreflect" -) - -func WriteMsg(s network.Stream, msg protoreflect.ProtoMessage) error { - w := pbio.NewDelimitedWriter(s) - return w.WriteMsg(msg) -} - -func ReadMsg(s network.Stream, msg protoreflect.ProtoMessage) error { - r := pbio.NewDelimitedReader(s, network.MessageSizeMax) - return r.ReadMsg(msg) -} diff --git a/network/address/README.md b/network/address/README.md deleted file mode 100644 index e3564de..0000000 --- a/network/address/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Addressing - -The `NodeID` is the Kademlia node identifier. It must implement a `Key()` method mapping the `NodeID` to a Kademlia Key. It can also contain additional information such as the node's network addresses. - -```go -// NodeID is a generic node identifier. It is used to identify a node and can -// also include extra information about the node, such as its network addresses. -type NodeID interface { - // Key returns the KadKey of the NodeID. - Key() key.KadKey - // String returns the string representation of the NodeID. String - // representation should be unique for each NodeID. - String() string -} -``` - -## `NodeID` implementation - -### `StringID` - -`StringID` uses a simple `string` as `NodeID`, and the `Key()` is derived using [SHA256](../../key/sha256key256/). - -### `KadID` - -`KadID` is a `KadKey` wrapper. It is useful in order to test different routing scenarios, because the `KadID` controls where the `Key` lies in the keyspace. The node's location in the keyspace is not random. - -### `PeerID` - -`PeerID` is a wrapper of the libp2p `peer.ID`. - -### `AddrInfo` - -`AddrInfo` is a wrapper of the libp2p `peer.AddrInfo`, containing a `peer.ID` and `[]multiaddr.multiaddr`. \ No newline at end of file diff --git a/network/address/address.go b/network/address/address.go deleted file mode 100644 index 28a08ec..0000000 --- a/network/address/address.go +++ /dev/null @@ -1,4 +0,0 @@ -package address - -// ProtocolID is a protocol identifier. -type ProtocolID string diff --git a/network/endpoint/README.md b/network/endpoint/README.md deleted file mode 100644 index f7491f0..0000000 --- a/network/endpoint/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Message Endpoint - -A message `Endpoint` handles everything about communications with remote Kademlia peers. They implement the abstraction of an address book to keep track of remote peers, and handle sending and receiving message. - -In order to have a `Server` node (responding to requests from other peers), a node must add `RequestHandler`s for specific `ProtocolID`s, and it will only answer requests for the supported protocols. A node is said to be in `Client` mode if it doesn't have request handlers for any `ProtocolID`. - -```go -// Endpoint defines how Kademlia nodes interacts with each other. -type Endpoint interface { - // MaybeAddToPeerstore adds the given address to the peerstore if it is - // valid and if it is not already there. - MaybeAddToPeerstore(context.Context, kad.NodeID, time.Duration) error - // SendRequestHandleResponse sends a request to the given peer and handles - // the response with the given handler. - SendRequestHandleResponse(context.Context, address.ProtocolID, kad.NodeID, - message.MinKadMessage, message.MinKadMessage, time.Duration, ResponseHandlerFn) - - // KadKey returns the KadKey of the local node. - KadKey() key.KadKey - // NetworkAddress returns the network address of the given peer (if known). - NetworkAddress(kad.NodeID) (kad.NodeID, error) -} - -// ServerEndpoint is a Kademlia endpoint that can handle requests from remote -// peers. -type ServerEndpoint interface { - Endpoint - // AddRequestHandler registers a handler for a given protocol ID. - AddRequestHandler(address.ProtocolID, RequestHandlerFn) - // RemoveRequestHandler removes a handler for a given protocol ID. - RemoveRequestHandler(address.ProtocolID) -} -``` - -## Implementations - -- **`Libp2pEndpoint`** is a message endpoint implementation based on Libp2p. -- **`FakeEndpoint`** is a simulated message endpoint, mostly used for tests and simulations. \ No newline at end of file diff --git a/network/endpoint/endpoint.go b/network/endpoint/endpoint.go deleted file mode 100644 index 3724bb4..0000000 --- a/network/endpoint/endpoint.go +++ /dev/null @@ -1,76 +0,0 @@ -package endpoint - -import ( - "context" - "time" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/network/address" -) - -// Connectedness signals the capacity for a connection with a given node. -// It is used to signal to services and other peers whether a node is reachable. -type Connectedness int - -const ( - // NotConnected means no connection to peer, and no extra information (default) - NotConnected Connectedness = iota - - // Connected means has an open, live connection to peer - Connected - - // CanConnect means recently connected to peer, terminated gracefully - CanConnect - - // CannotConnect means recently attempted connecting but failed to connect. - // (should signal "made effort, failed") - CannotConnect -) - -// RequestHandlerFn defines a function that handles a request from a remote peer -type RequestHandlerFn[K kad.Key[K]] func(context.Context, kad.NodeID[K], - kad.Message) (kad.Message, error) - -// ResponseHandlerFn defines a function that deals with the response to a -// request previously sent to a remote peer. -type ResponseHandlerFn[K kad.Key[K], A kad.Address[A]] func(context.Context, kad.Response[K, A], error) - -// Endpoint defines how Kademlia nodes interacts with each other. -type Endpoint[K kad.Key[K], A kad.Address[A]] interface { - // MaybeAddToPeerstore adds the given address to the peerstore if it is - // valid and if it is not already there. - // TODO: consider returning a status of whether the nodeinfo is a new node or contains a new address - MaybeAddToPeerstore(context.Context, kad.NodeInfo[K, A], time.Duration) error - - // SendRequestHandleResponse attempts to sends a request to the given peer and handles - // the response with the given handler. - // An error is returned if the endpoint is unable to initiate sending the message for - // any reason. The handler will not be called if an error is returned. - SendRequestHandleResponse(context.Context, address.ProtocolID, kad.NodeID[K], - kad.Message, kad.Message, time.Duration, - ResponseHandlerFn[K, A]) error - - // NetworkAddress returns the network address of the given peer (if known). - NetworkAddress(kad.NodeID[K]) (kad.NodeInfo[K, A], error) -} - -// ServerEndpoint is a Kademlia endpoint that can handle requests from remote -// peers. -type ServerEndpoint[K kad.Key[K], A kad.Address[A]] interface { - Endpoint[K, A] - // AddRequestHandler registers a handler for a given protocol ID. - AddRequestHandler(address.ProtocolID, kad.Message, RequestHandlerFn[K]) error - // RemoveRequestHandler removes a handler for a given protocol ID. - RemoveRequestHandler(address.ProtocolID) -} - -// NetworkedEndpoint is an endpoint keeping track of the connectedness with -// known remote peers. -type NetworkedEndpoint[K kad.Key[K], A kad.Address[A]] interface { - Endpoint[K, A] - // Connectedness returns the connectedness of the given peer. - Connectedness(kad.NodeID[K]) (Connectedness, error) -} - -// StreamID is a unique identifier for a stream. -type StreamID uint64 diff --git a/network/endpoint/errors.go b/network/endpoint/errors.go deleted file mode 100644 index 851b482..0000000 --- a/network/endpoint/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package endpoint - -import "errors" - -var ( - ErrCannotConnect = errors.New("cannot connect") - ErrUnknownPeer = errors.New("unknown peer") - ErrInvalidPeer = errors.New("invalid peer") - ErrTimeout = errors.New("request timeout") - ErrNilRequestHandler = errors.New("nil request handler") - ErrNilResponseHandler = errors.New("nil response handler") - ErrResponseReceivedAfterTimeout = errors.New("response received after timeout") -) diff --git a/node.go b/node.go new file mode 100644 index 0000000..e88b87b --- /dev/null +++ b/node.go @@ -0,0 +1,26 @@ +package libdht + +// NodeID is a generic node identifier and not equal to a Kademlia key. Some +// implementations use NodeID's as preimages for Kademlia keys. Kademlia keys +// are used for calculating distances between nodes while NodeID's are the +// original logical identifier of a node. +// +// The NodeID interface only defines a method that returns the Kademlia key +// for the given NodeID. E.g., the operation to go from a NodeID to a Kademlia key +// can be as simple as hashing the NodeID. +// +// Implementations may choose to equate NodeID's and Kademlia keys. +type NodeID[D Distance[D], P Point[P, D]] interface { + // Key returns the Kademlia key of the given NodeID. E.g., NodeID's can be + // preimages to Kademlia keys, in which case, Key() could return the SHA256 + // of NodeID. + Key() P +} + +type Request[D Distance[D], P Point[P, D], N NodeID[D, P]] interface { + Target() P +} + +type Response[D Distance[D], P Point[P, D], N NodeID[D, P]] interface { + CloserNodes() []N +} diff --git a/point.go b/point.go new file mode 100644 index 0000000..42ed152 --- /dev/null +++ b/point.go @@ -0,0 +1,14 @@ +package libdht + +type Distance[D any] interface { + // Compare compares the numeric value of the key with another key of the same type. + // It returns -1 if the key is numerically less than other, +1 if it is greater + // and 0 if both keys are equal. + Compare(other D) int +} + +type Point[P any, D Distance[D]] interface { + // Distance returns a Distance between two points + Distance(P) D + Equal(P) bool +} diff --git a/query/README.md b/query/README.md deleted file mode 100644 index 904d599..0000000 --- a/query/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Query - -This folder contains multiple `Query` mechanism implementations. - -## Implementations - -- **SimpleQuery** is a simple query mechanism. \ No newline at end of file diff --git a/query/iter.go b/query/iter.go deleted file mode 100644 index eaa11b9..0000000 --- a/query/iter.go +++ /dev/null @@ -1,103 +0,0 @@ -package query - -import ( - "context" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/key/trie" -) - -// A NodeIter iterates nodes according to some strategy. -type NodeIter[K kad.Key[K]] interface { - // Add adds node information to the iterator - Add(*NodeStatus[K]) - - // Find returns the node information corresponding to the given Kademlia key - Find(K) (*NodeStatus[K], bool) - - // Each applies fn to each entry in the iterator in order. Each stops and returns true if fn returns true. - // Otherwise Each returns false when there are no further entries. - Each(ctx context.Context, fn func(context.Context, *NodeStatus[K]) bool) bool -} - -// A ClosestNodesIter iterates nodes in order of ascending distance from a key. -type ClosestNodesIter[K kad.Key[K]] struct { - // target is the key whose distance to a node determines the position of that node in the iterator. - target K - - // nodelist holds the nodes discovered so far, ordered by increasing distance from the target. - nodes *trie.Trie[K, *NodeStatus[K]] -} - -var _ NodeIter[key.Key8] = (*ClosestNodesIter[key.Key8])(nil) - -// NewClosestNodesIter creates a new ClosestNodesIter -func NewClosestNodesIter[K kad.Key[K]](target K) *ClosestNodesIter[K] { - return &ClosestNodesIter[K]{ - target: target, - nodes: trie.New[K, *NodeStatus[K]](), - } -} - -func (iter *ClosestNodesIter[K]) Add(ni *NodeStatus[K]) { - iter.nodes.Add(ni.NodeID.Key(), ni) -} - -func (iter *ClosestNodesIter[K]) Find(k K) (*NodeStatus[K], bool) { - found, ni := trie.Find(iter.nodes, k) - return ni, found -} - -func (iter *ClosestNodesIter[K]) Each(ctx context.Context, fn func(context.Context, *NodeStatus[K]) bool) bool { - // get all the nodes in order of distance from the target - // TODO: turn this into a walk or iterator on trie.Trie - entries := trie.Closest(iter.nodes, iter.target, iter.nodes.Size()) - for _, e := range entries { - ni := e.Data - if fn(ctx, ni) { - return true - } - } - return false -} - -// A SequentialIter iterates nodes in the order they were added to the iterator. -type SequentialIter[K kad.Key[K]] struct { - // nodelist holds the nodes discovered so far, ordered by increasing distance from the target. - nodes []*NodeStatus[K] -} - -var _ NodeIter[key.Key8] = (*SequentialIter[key.Key8])(nil) - -// NewSequentialIter creates a new SequentialIter -func NewSequentialIter[K kad.Key[K]]() *SequentialIter[K] { - return &SequentialIter[K]{ - nodes: make([]*NodeStatus[K], 0), - } -} - -func (iter *SequentialIter[K]) Add(ni *NodeStatus[K]) { - iter.nodes = append(iter.nodes, ni) -} - -// Find returns the node information corresponding to the given Kademlia key. It uses a linear -// search which makes it unsuitable for large numbers of entries. -func (iter *SequentialIter[K]) Find(k K) (*NodeStatus[K], bool) { - for i := range iter.nodes { - if key.Equal(k, iter.nodes[i].NodeID.Key()) { - return iter.nodes[i], true - } - } - - return nil, false -} - -func (iter *SequentialIter[K]) Each(ctx context.Context, fn func(context.Context, *NodeStatus[K]) bool) bool { - for _, ns := range iter.nodes { - if fn(ctx, ns) { - return true - } - } - return false -} diff --git a/query/iter_test.go b/query/iter_test.go deleted file mode 100644 index 6cecb29..0000000 --- a/query/iter_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package query - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/key" -) - -func TestClosestNodesIter(t *testing.T) { - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - // ensure the order of the known nodes - require.True(t, target.Xor(a.Key()).Compare(target.Xor(b.Key())) == -1) - require.True(t, target.Xor(b.Key()).Compare(target.Xor(c.Key())) == -1) - require.True(t, target.Xor(c.Key()).Compare(target.Xor(d.Key())) == -1) - - iter := NewClosestNodesIter(target) - - // add nodes in "random order" - - iter.Add(&NodeStatus[key.Key8]{NodeID: b}) - iter.Add(&NodeStatus[key.Key8]{NodeID: d}) - iter.Add(&NodeStatus[key.Key8]{NodeID: a}) - iter.Add(&NodeStatus[key.Key8]{NodeID: c}) - - // Each should iterate in order of distance from target - - distances := make([]key.Key8, 0, 4) - iter.Each(context.Background(), func(ctx context.Context, ns *NodeStatus[key.Key8]) bool { - distances = append(distances, target.Xor(ns.NodeID.Key())) - return false - }) - - require.True(t, key.IsSorted(distances)) -} - -func TestSequentialIter(t *testing.T) { - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - iter := NewSequentialIter[key.Key8]() - - // add nodes in "random order" - - iter.Add(&NodeStatus[key.Key8]{NodeID: b}) - iter.Add(&NodeStatus[key.Key8]{NodeID: d}) - iter.Add(&NodeStatus[key.Key8]{NodeID: a}) - iter.Add(&NodeStatus[key.Key8]{NodeID: c}) - - // Each should iterate in order the nodes were added to the iiterator - - order := make([]key.Key8, 0, 4) - iter.Each(context.Background(), func(ctx context.Context, ns *NodeStatus[key.Key8]) bool { - order = append(order, ns.NodeID.Key()) - return false - }) - - require.Equal(t, 4, len(order)) - require.True(t, key.Equal(order[0], b.Key())) - require.True(t, key.Equal(order[1], d.Key())) - require.True(t, key.Equal(order[2], a.Key())) - require.True(t, key.Equal(order[3], c.Key())) -} diff --git a/query/node.go b/query/node.go deleted file mode 100644 index 0fd5960..0000000 --- a/query/node.go +++ /dev/null @@ -1,40 +0,0 @@ -package query - -import ( - "time" - - "github.com/plprobelab/go-kademlia/kad" -) - -type NodeStatus[K kad.Key[K]] struct { - NodeID kad.NodeID[K] - State NodeState -} - -type NodeState interface { - nodeState() -} - -// StateNodeNotContacted indicates that the node has not been contacted yet. -type StateNodeNotContacted struct{} - -// StateNodeWaiting indicates that a query is waiting for a response from the node. -type StateNodeWaiting struct { - Deadline time.Time -} - -// StateNodeUnresponsive indicates that the node did not respond within the configured timeout. -type StateNodeUnresponsive struct{} - -// StateNodeFailed indicates that the attempt to contact the node failed. -type StateNodeFailed struct{} - -// StateNodeSucceeded indicates that the attempt to contact the node succeeded. -type StateNodeSucceeded struct{} - -// nodeState() ensures that only node states can be assigned to a nodeState interface. -func (*StateNodeNotContacted) nodeState() {} -func (*StateNodeWaiting) nodeState() {} -func (*StateNodeUnresponsive) nodeState() {} -func (*StateNodeFailed) nodeState() {} -func (*StateNodeSucceeded) nodeState() {} diff --git a/query/pool.go b/query/pool.go deleted file mode 100644 index 7e6a469..0000000 --- a/query/pool.go +++ /dev/null @@ -1,359 +0,0 @@ -package query - -import ( - "context" - "fmt" - "time" - - "github.com/benbjohnson/clock" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/kaderr" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/util" -) - -type Pool[K kad.Key[K], A kad.Address[A]] struct { - // self is the node id of the system the pool is running on - self kad.NodeID[K] - queries []*Query[K, A] - queryIndex map[QueryID]*Query[K, A] - - // cfg is a copy of the optional configuration supplied to the pool - cfg PoolConfig - - // queriesInFlight is number of queries that are waiting for message responses - queriesInFlight int -} - -// PoolConfig specifies optional configuration for a Pool -type PoolConfig struct { - Concurrency int // the maximum number of queries that may be waiting for message responses at any one time - Timeout time.Duration // the time to wait before terminating a query that is not making progress - Replication int // the 'k' parameter defined by Kademlia - QueryConcurrency int // the maximum number of concurrent requests that each query may have in flight - RequestTimeout time.Duration // the timeout queries should use for contacting a single node - Clock clock.Clock // a clock that may replaced by a mock when testing -} - -// Validate checks the configuration options and returns an error if any have invalid values. -func (cfg *PoolConfig) Validate() error { - if cfg.Clock == nil { - return &kaderr.ConfigurationError{ - Component: "PoolConfig", - Err: fmt.Errorf("clock must not be nil"), - } - } - if cfg.Concurrency < 1 { - return &kaderr.ConfigurationError{ - Component: "PoolConfig", - Err: fmt.Errorf("concurrency must be greater than zero"), - } - } - if cfg.Timeout < 1 { - return &kaderr.ConfigurationError{ - Component: "PoolConfig", - Err: fmt.Errorf("timeout must be greater than zero"), - } - } - if cfg.Replication < 1 { - return &kaderr.ConfigurationError{ - Component: "PoolConfig", - Err: fmt.Errorf("replication must be greater than zero"), - } - } - - if cfg.QueryConcurrency < 1 { - return &kaderr.ConfigurationError{ - Component: "PoolConfig", - Err: fmt.Errorf("query concurrency must be greater than zero"), - } - } - - if cfg.RequestTimeout < 1 { - return &kaderr.ConfigurationError{ - Component: "PoolConfig", - Err: fmt.Errorf("request timeout must be greater than zero"), - } - } - - return nil -} - -// DefaultPoolConfig returns the default configuration options for a Pool. -// Options may be overridden before passing to NewPool -func DefaultPoolConfig() *PoolConfig { - return &PoolConfig{ - Clock: clock.New(), // use standard time - Concurrency: 3, - Timeout: 5 * time.Minute, - Replication: 20, - QueryConcurrency: 3, - RequestTimeout: time.Minute, - } -} - -func NewPool[K kad.Key[K], A kad.Address[A]](self kad.NodeID[K], cfg *PoolConfig) (*Pool[K, A], error) { - if cfg == nil { - cfg = DefaultPoolConfig() - } else if err := cfg.Validate(); err != nil { - return nil, err - } - - return &Pool[K, A]{ - self: self, - cfg: *cfg, - queries: make([]*Query[K, A], 0), - queryIndex: make(map[QueryID]*Query[K, A]), - }, nil -} - -// Advance advances the state of the pool by attempting to advance one of its queries -func (p *Pool[K, A]) Advance(ctx context.Context, ev PoolEvent) PoolState { - ctx, span := util.StartSpan(ctx, "Pool.Advance") - defer span.End() - - // reset the in flight counter so it can be calculated as the queries are advanced - p.queriesInFlight = 0 - - // eventQueryID keeps track of a query that was advanced via a specific event, to avoid it - // being advanced twice - eventQueryID := InvalidQueryID - - switch tev := ev.(type) { - case *EventPoolAddQuery[K, A]: - p.addQuery(ctx, tev.QueryID, tev.Target, tev.ProtocolID, tev.Message, tev.KnownClosestNodes) - // TODO: return error as state - case *EventPoolStopQuery: - if qry, ok := p.queryIndex[tev.QueryID]; ok { - state, terminal := p.advanceQuery(ctx, qry, &EventQueryCancel{}) - if terminal { - return state - } - eventQueryID = qry.id - } - case *EventPoolMessageResponse[K, A]: - if qry, ok := p.queryIndex[tev.QueryID]; ok { - state, terminal := p.advanceQuery(ctx, qry, &EventQueryMessageResponse[K, A]{ - NodeID: tev.NodeID, - Response: tev.Response, - }) - if terminal { - return state - } - eventQueryID = qry.id - } - case *EventPoolMessageFailure[K]: - if qry, ok := p.queryIndex[tev.QueryID]; ok { - state, terminal := p.advanceQuery(ctx, qry, &EventQueryMessageFailure[K]{ - NodeID: tev.NodeID, - Error: tev.Error, - }) - if terminal { - return state - } - eventQueryID = qry.id - } - case *EventPoolPoll: - // no event to process - default: - panic(fmt.Sprintf("unexpected event: %T", tev)) - } - - if len(p.queries) == 0 { - return &StatePoolIdle{} - } - - // Attempt to advance another query - for _, qry := range p.queries { - if eventQueryID == qry.id { - // avoid advancing query twice - continue - } - - state, terminal := p.advanceQuery(ctx, qry, nil) - if terminal { - return state - } - - // check if we have the maximum number of queries in flight - if p.queriesInFlight >= p.cfg.Concurrency { - return &StatePoolWaitingAtCapacity{} - } - } - - if p.queriesInFlight > 0 { - return &StatePoolWaitingWithCapacity{} - } - - return &StatePoolIdle{} -} - -func (p *Pool[K, A]) advanceQuery(ctx context.Context, qry *Query[K, A], qev QueryEvent) (PoolState, bool) { - state := qry.Advance(ctx, qev) - switch st := state.(type) { - case *StateQueryWaitingMessage[K, A]: - p.queriesInFlight++ - return &StatePoolQueryMessage[K, A]{ - QueryID: st.QueryID, - Stats: st.Stats, - NodeID: st.NodeID, - ProtocolID: st.ProtocolID, - Message: st.Message, - }, true - case *StateQueryFinished: - p.removeQuery(qry.id) - return &StatePoolQueryFinished{ - QueryID: st.QueryID, - Stats: st.Stats, - }, true - case *StateQueryWaitingAtCapacity: - elapsed := p.cfg.Clock.Since(qry.stats.Start) - if elapsed > p.cfg.Timeout { - p.removeQuery(qry.id) - return &StatePoolQueryTimeout{ - QueryID: st.QueryID, - Stats: st.Stats, - }, true - } - p.queriesInFlight++ - case *StateQueryWaitingWithCapacity: - elapsed := p.cfg.Clock.Since(qry.stats.Start) - if elapsed > p.cfg.Timeout { - p.removeQuery(qry.id) - return &StatePoolQueryTimeout{ - QueryID: st.QueryID, - Stats: st.Stats, - }, true - } - p.queriesInFlight++ - } - return nil, false -} - -func (p *Pool[K, A]) removeQuery(queryID QueryID) { - for i := range p.queries { - if p.queries[i].id != queryID { - continue - } - // remove from slice - copy(p.queries[i:], p.queries[i+1:]) - p.queries[len(p.queries)-1] = nil - p.queries = p.queries[:len(p.queries)-1] - break - } - delete(p.queryIndex, queryID) -} - -// addQuery adds a query to the pool, returning the new query id -// TODO: remove target argument and use msg.Target -func (p *Pool[K, A]) addQuery(ctx context.Context, queryID QueryID, target K, protocolID address.ProtocolID, msg kad.Request[K, A], knownClosestNodes []kad.NodeID[K]) error { - if _, exists := p.queryIndex[queryID]; exists { - return fmt.Errorf("query id already in use") - } - iter := NewClosestNodesIter(target) - - qryCfg := DefaultQueryConfig[K]() - qryCfg.Clock = p.cfg.Clock - qryCfg.Concurrency = p.cfg.QueryConcurrency - qryCfg.RequestTimeout = p.cfg.RequestTimeout - - qry, err := NewQuery[K](p.self, queryID, protocolID, msg, iter, knownClosestNodes, qryCfg) - if err != nil { - return fmt.Errorf("new query: %w", err) - } - - p.queries = append(p.queries, qry) - p.queryIndex[queryID] = qry - - return nil -} - -// States - -type PoolState interface { - poolState() -} - -// StatePoolIdle indicates that the pool is idle, i.e. there are no queries to process. -type StatePoolIdle struct{} - -// StatePoolQueryMessage indicates that a pool query is waiting to message a node. -type StatePoolQueryMessage[K kad.Key[K], A kad.Address[A]] struct { - QueryID QueryID - NodeID kad.NodeID[K] - ProtocolID address.ProtocolID - Message kad.Request[K, A] - Stats QueryStats -} - -// StatePoolWaitingAtCapacity indicates that at least one query is waiting for results and the pool has reached -// its maximum number of concurrent queries. -type StatePoolWaitingAtCapacity struct{} - -// StatePoolWaitingWithCapacity indicates that at least one query is waiting for results but capacity to -// start more is available. -type StatePoolWaitingWithCapacity struct{} - -// StatePoolQueryFinished indicates that a query has finished. -type StatePoolQueryFinished struct { - QueryID QueryID - Stats QueryStats -} - -// StatePoolQueryTimeout indicates that a query has timed out. -type StatePoolQueryTimeout struct { - QueryID QueryID - Stats QueryStats -} - -// poolState() ensures that only Pool states can be assigned to the PoolState interface. -func (*StatePoolIdle) poolState() {} -func (*StatePoolQueryMessage[K, A]) poolState() {} -func (*StatePoolWaitingAtCapacity) poolState() {} -func (*StatePoolWaitingWithCapacity) poolState() {} -func (*StatePoolQueryFinished) poolState() {} -func (*StatePoolQueryTimeout) poolState() {} - -// PoolEvent is an event intended to advance the state of a pool. -type PoolEvent interface { - poolEvent() -} - -// EventPoolAddQuery is an event that attempts to add a new query -type EventPoolAddQuery[K kad.Key[K], A kad.Address[A]] struct { - QueryID QueryID // the id to use for the new query - Target K // the target key for the query - ProtocolID address.ProtocolID // the protocol that defines how the message should be interpreted - Message kad.Request[K, A] // the message the query should send to each node it traverses - KnownClosestNodes []kad.NodeID[K] // an initial set of close nodes the query should use -} - -// EventPoolStopQuery notifies a pool to stop a query. -type EventPoolStopQuery struct { - QueryID QueryID // the id of the query that should be stopped -} - -// EventPoolMessageResponse notifies a pool that a query that a sent message has received a successful response. -type EventPoolMessageResponse[K kad.Key[K], A kad.Address[A]] struct { - QueryID QueryID // the id of the query that sent the message - NodeID kad.NodeID[K] // the node the message was sent to - Response kad.Response[K, A] // the message response sent by the node -} - -// EventPoolMessageFailure notifies a pool that a query that an attempt to send a message has failed. -type EventPoolMessageFailure[K kad.Key[K]] struct { - QueryID QueryID // the id of the query that sent the message - NodeID kad.NodeID[K] // the node the message was sent to - Error error // the error that caused the failure, if any -} - -// EventPoolPoll is an event that signals the pool that it can perform housekeeping work such as time out queries. -type EventPoolPoll struct{} - -// poolEvent() ensures that only Pool events can be assigned to the PoolEvent interface. -func (*EventPoolAddQuery[K, A]) poolEvent() {} -func (*EventPoolStopQuery) poolEvent() {} -func (*EventPoolMessageResponse[K, A]) poolEvent() {} -func (*EventPoolMessageFailure[K]) poolEvent() {} -func (*EventPoolPoll) poolEvent() {} diff --git a/query/pool_test.go b/query/pool_test.go deleted file mode 100644 index 3e8da96..0000000 --- a/query/pool_test.go +++ /dev/null @@ -1,370 +0,0 @@ -package query - -import ( - "context" - "testing" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" -) - -func TestPoolConfigValidate(t *testing.T) { - t.Run("default is valid", func(t *testing.T) { - cfg := DefaultPoolConfig() - require.NoError(t, cfg.Validate()) - }) - - t.Run("clock is not nil", func(t *testing.T) { - cfg := DefaultPoolConfig() - cfg.Clock = nil - require.Error(t, cfg.Validate()) - }) - - t.Run("concurrency positive", func(t *testing.T) { - cfg := DefaultPoolConfig() - cfg.Concurrency = 0 - require.Error(t, cfg.Validate()) - cfg.Concurrency = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("timeout positive", func(t *testing.T) { - cfg := DefaultPoolConfig() - cfg.Timeout = 0 - require.Error(t, cfg.Validate()) - cfg.Timeout = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("replication positive", func(t *testing.T) { - cfg := DefaultPoolConfig() - cfg.Replication = 0 - require.Error(t, cfg.Validate()) - cfg.Replication = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("query concurrency positive", func(t *testing.T) { - cfg := DefaultPoolConfig() - cfg.QueryConcurrency = 0 - require.Error(t, cfg.Validate()) - cfg.QueryConcurrency = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("request timeout positive", func(t *testing.T) { - cfg := DefaultPoolConfig() - cfg.RequestTimeout = 0 - require.Error(t, cfg.Validate()) - cfg.RequestTimeout = -1 - require.Error(t, cfg.Validate()) - }) -} - -func TestPoolStartsIdle(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultPoolConfig() - cfg.Clock = clk - - self := kadtest.NewID(key.Key8(0)) - p, err := NewPool[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - state := p.Advance(ctx, &EventPoolPoll{}) - require.IsType(t, &StatePoolIdle{}, state) -} - -func TestPoolStopWhenNoQueries(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultPoolConfig() - cfg.Clock = clk - - self := kadtest.NewID(key.Key8(0)) - p, err := NewPool[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - state := p.Advance(ctx, &EventPoolPoll{}) - require.IsType(t, &StatePoolIdle{}, state) -} - -func TestPoolAddQueryStartsIfCapacity(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultPoolConfig() - cfg.Clock = clk - - self := kadtest.NewID(key.Key8(0)) - p, err := NewPool[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - // first thing the new pool should do is start the query - state := p.Advance(ctx, &EventPoolAddQuery[key.Key8, kadtest.StrAddr]{ - QueryID: queryID, - Target: target, - ProtocolID: protocolID, - Message: msg, - KnownClosestNodes: []kad.NodeID[key.Key8]{a}, - }) - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - - // the query should attempt to contact the node it was given - st := state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - - // the query should be the one just added - require.Equal(t, queryID, st.QueryID) - - // the query should attempt to contact the node it was given - require.Equal(t, a, st.NodeID) - - // with the correct protocol ID - require.Equal(t, protocolID, st.ProtocolID) - - // with the correct message - require.Equal(t, msg, st.Message) - - // now the pool reports that it is waiting - state = p.Advance(ctx, &EventPoolPoll{}) - require.IsType(t, &StatePoolWaitingWithCapacity{}, state) -} - -func TestPoolMessageResponse(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultPoolConfig() - cfg.Clock = clk - - self := kadtest.NewID(key.Key8(0)) - p, err := NewPool[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - // first thing the new pool should do is start the query - state := p.Advance(ctx, &EventPoolAddQuery[key.Key8, kadtest.StrAddr]{ - QueryID: queryID, - Target: target, - ProtocolID: protocolID, - Message: msg, - KnownClosestNodes: []kad.NodeID[key.Key8]{a}, - }) - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - - // the query should attempt to contact the node it was given - st := state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID, st.QueryID) - require.Equal(t, a, st.NodeID) - - // notify query that node was contacted successfully, but no closer nodes - state = p.Advance(ctx, &EventPoolMessageResponse[key.Key8, kadtest.StrAddr]{ - QueryID: queryID, - NodeID: a, - }) - - // pool should respond that query has finished - require.IsType(t, &StatePoolQueryFinished{}, state) - - stf := state.(*StatePoolQueryFinished) - require.Equal(t, queryID, stf.QueryID) - require.Equal(t, 1, stf.Stats.Requests) - require.Equal(t, 1, stf.Stats.Success) -} - -func TestPoolPrefersRunningQueriesOverNewOnes(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultPoolConfig() - cfg.Clock = clk - cfg.Concurrency = 2 // allow two queries to run concurrently - - self := kadtest.NewID(key.Key8(0)) - p, err := NewPool[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - msg1 := kadtest.NewRequest("1", target) - queryID1 := QueryID("1") - - protocolID := address.ProtocolID("testprotocol") - - // Add the first query - state := p.Advance(ctx, &EventPoolAddQuery[key.Key8, kadtest.StrAddr]{ - QueryID: queryID1, - Target: target, - ProtocolID: protocolID, - Message: msg1, - KnownClosestNodes: []kad.NodeID[key.Key8]{a, b, c, d}, - }) - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - - // the first query should attempt to contact the node it was given - st := state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID1, st.QueryID) - require.Equal(t, a, st.NodeID) - - msg2 := kadtest.NewRequest("2", target) - queryID2 := QueryID("2") - - // Add the second query - state = p.Advance(ctx, &EventPoolAddQuery[key.Key8, kadtest.StrAddr]{ - QueryID: queryID2, - Target: target, - ProtocolID: protocolID, - Message: msg2, - KnownClosestNodes: []kad.NodeID[key.Key8]{a, b, c, d}, - }) - - // the first query should continue its operation in preference to starting the new query - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID1, st.QueryID) - require.Equal(t, b, st.NodeID) - - // advance the pool again, the first query should continue its operation in preference to starting the new query - state = p.Advance(ctx, &EventPoolPoll{}) - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID1, st.QueryID) - require.Equal(t, c, st.NodeID) - - // advance the pool again, the first query is at capacity so the second query can start - state = p.Advance(ctx, &EventPoolPoll{}) - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID2, st.QueryID) - require.Equal(t, a, st.NodeID) - - // notify first query that node was contacted successfully, but no closer nodes - state = p.Advance(ctx, &EventPoolMessageResponse[key.Key8, kadtest.StrAddr]{ - QueryID: queryID1, - NodeID: a, - }) - - // first query starts a new message request - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID1, st.QueryID) - require.Equal(t, d, st.NodeID) - - // notify first query that next node was contacted successfully, but no closer nodes - state = p.Advance(ctx, &EventPoolMessageResponse[key.Key8, kadtest.StrAddr]{ - QueryID: queryID1, - NodeID: b, - }) - - // first query is out of nodes to try so second query can proceed - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID2, st.QueryID) - require.Equal(t, b, st.NodeID) -} - -func TestPoolRespectsConcurrency(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultPoolConfig() - cfg.Clock = clk - cfg.Concurrency = 2 // allow two queries to run concurrently - cfg.QueryConcurrency = 1 // allow each query to have a single request in flight - - self := kadtest.NewID(key.Key8(0)) - p, err := NewPool[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - - msg1 := kadtest.NewRequest("1", target) - queryID1 := QueryID("1") - - protocolID := address.ProtocolID("testprotocol") - - // Add the first query - state := p.Advance(ctx, &EventPoolAddQuery[key.Key8, kadtest.StrAddr]{ - QueryID: queryID1, - Target: target, - ProtocolID: protocolID, - Message: msg1, - KnownClosestNodes: []kad.NodeID[key.Key8]{a}, - }) - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - - // the first query should attempt to contact the node it was given - st := state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID1, st.QueryID) - require.Equal(t, a, st.NodeID) - - msg2 := kadtest.NewRequest("2", target) - queryID2 := QueryID("2") - - // Add the second query - state = p.Advance(ctx, &EventPoolAddQuery[key.Key8, kadtest.StrAddr]{ - QueryID: queryID2, - Target: target, - ProtocolID: protocolID, - Message: msg2, - KnownClosestNodes: []kad.NodeID[key.Key8]{a}, - }) - - // the second query should start since the first query has a request in flight - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID2, st.QueryID) - require.Equal(t, a, st.NodeID) - - msg3 := kadtest.NewRequest("3", target) - queryID3 := QueryID("3") - - // Add a third query - state = p.Advance(ctx, &EventPoolAddQuery[key.Key8, kadtest.StrAddr]{ - QueryID: queryID3, - Target: target, - ProtocolID: protocolID, - Message: msg3, - KnownClosestNodes: []kad.NodeID[key.Key8]{a}, - }) - - // the third query should wait since the pool has reached maximum concurrency - require.IsType(t, &StatePoolWaitingAtCapacity{}, state) - - // notify first query that next node was contacted successfully, but no closer nodes - state = p.Advance(ctx, &EventPoolMessageResponse[key.Key8, kadtest.StrAddr]{ - QueryID: queryID1, - NodeID: a, - }) - - // first query is out of nodes so it has finished - require.IsType(t, &StatePoolQueryFinished{}, state) - stf := state.(*StatePoolQueryFinished) - require.Equal(t, queryID1, stf.QueryID) - - // advancing pool again allows query 3 to start - state = p.Advance(ctx, &EventPoolPoll{}) - require.IsType(t, &StatePoolQueryMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StatePoolQueryMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID3, st.QueryID) - require.Equal(t, a, st.NodeID) -} diff --git a/query/query.go b/query/query.go deleted file mode 100644 index f637f21..0000000 --- a/query/query.go +++ /dev/null @@ -1,394 +0,0 @@ -package query - -import ( - "context" - "fmt" - "time" - - "github.com/benbjohnson/clock" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/kaderr" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/util" -) - -type QueryID string - -const InvalidQueryID QueryID = "" - -type QueryStats struct { - Start time.Time - End time.Time - Requests int - Success int - Failure int -} - -type QueryState interface { - queryState() -} - -// StateQueryFinished indicates that the Query has finished. -type StateQueryFinished struct { - QueryID QueryID - Stats QueryStats -} - -// StateQueryWaitingMessage indicates that the Query is waiting to send a message to a node. -type StateQueryWaitingMessage[K kad.Key[K], A kad.Address[A]] struct { - QueryID QueryID - Stats QueryStats - NodeID kad.NodeID[K] - ProtocolID address.ProtocolID - Message kad.Request[K, A] -} - -// StateQueryWaitingAtCapacity indicates that the Query is waiting for results and is at capacity. -type StateQueryWaitingAtCapacity struct { - QueryID QueryID - Stats QueryStats -} - -// StateQueryWaitingWithCapacity indicates that the Query is waiting for results but has no further nodes to contact. -type StateQueryWaitingWithCapacity struct { - QueryID QueryID - Stats QueryStats -} - -// queryState() ensures that only Query states can be assigned to a QueryState. -func (*StateQueryFinished) queryState() {} -func (*StateQueryWaitingMessage[K, A]) queryState() {} -func (*StateQueryWaitingAtCapacity) queryState() {} -func (*StateQueryWaitingWithCapacity) queryState() {} - -type QueryEvent interface { - queryEvent() -} - -// EventQueryMessageResponse notifies a query to stop all work and enter the finished state. -type EventQueryCancel struct{} - -// EventQueryMessageResponse notifies a query that an attempt to send a message has received a successful response. -type EventQueryMessageResponse[K kad.Key[K], A kad.Address[A]] struct { - NodeID kad.NodeID[K] // the node the message was sent to - Response kad.Response[K, A] // the message response sent by the node -} - -// EventQueryMessageFailure notifies a query that an attempt to send a message has failed. -type EventQueryMessageFailure[K kad.Key[K]] struct { - NodeID kad.NodeID[K] // the node the message was sent to - Error error // the error that caused the failure, if any -} - -// queryEvent() ensures that only Query events can be assigned to a QueryEvent. -func (*EventQueryCancel) queryEvent() {} -func (*EventQueryMessageResponse[K, A]) queryEvent() {} -func (*EventQueryMessageFailure[K]) queryEvent() {} - -// QueryConfig specifies optional configuration for a Query -type QueryConfig[K kad.Key[K]] struct { - Concurrency int // the maximum number of concurrent requests that may be in flight - NumResults int // the minimum number of nodes to successfully contact before considering iteration complete - RequestTimeout time.Duration // the timeout for contacting a single node - Clock clock.Clock // a clock that may replaced by a mock when testing -} - -// Validate checks the configuration options and returns an error if any have invalid values. -func (cfg *QueryConfig[K]) Validate() error { - if cfg.Clock == nil { - return &kaderr.ConfigurationError{ - Component: "QueryConfig", - Err: fmt.Errorf("clock must not be nil"), - } - } - if cfg.Concurrency < 1 { - return &kaderr.ConfigurationError{ - Component: "QueryConfig", - Err: fmt.Errorf("concurrency must be greater than zero"), - } - } - if cfg.NumResults < 1 { - return &kaderr.ConfigurationError{ - Component: "QueryConfig", - Err: fmt.Errorf("num results must be greater than zero"), - } - } - if cfg.RequestTimeout < 1 { - return &kaderr.ConfigurationError{ - Component: "QueryConfig", - Err: fmt.Errorf("request timeout must be greater than zero"), - } - } - return nil -} - -// DefaultQueryConfig returns the default configuration options for a Query. -// Options may be overridden before passing to NewQuery -func DefaultQueryConfig[K kad.Key[K]]() *QueryConfig[K] { - return &QueryConfig[K]{ - Concurrency: 3, - NumResults: 20, - RequestTimeout: time.Minute, - Clock: clock.New(), // use standard time - } -} - -type Query[K kad.Key[K], A kad.Address[A]] struct { - self kad.NodeID[K] - id QueryID - - // cfg is a copy of the optional configuration supplied to the query - cfg QueryConfig[K] - - iter NodeIter[K] - protocolID address.ProtocolID - msg kad.Request[K, A] - stats QueryStats - - // finished indicates that that the query has completed its work or has been stopped. - finished bool - - // inFlight is number of requests in flight, will be <= concurrency - inFlight int -} - -func NewQuery[K kad.Key[K], A kad.Address[A]](self kad.NodeID[K], id QueryID, protocolID address.ProtocolID, msg kad.Request[K, A], iter NodeIter[K], knownClosestNodes []kad.NodeID[K], cfg *QueryConfig[K]) (*Query[K, A], error) { - if cfg == nil { - cfg = DefaultQueryConfig[K]() - } else if err := cfg.Validate(); err != nil { - return nil, err - } - - for _, node := range knownClosestNodes { - // exclude self from closest nodes - if key.Equal(node.Key(), self.Key()) { - continue - } - iter.Add(&NodeStatus[K]{ - NodeID: node, - State: &StateNodeNotContacted{}, - }) - } - - return &Query[K, A]{ - self: self, - id: id, - cfg: *cfg, - iter: iter, - protocolID: protocolID, - msg: msg, - }, nil -} - -func (q *Query[K, A]) Advance(ctx context.Context, ev QueryEvent) QueryState { - ctx, span := util.StartSpan(ctx, "Query.Advance") - defer span.End() - if q.finished { - return &StateQueryFinished{ - QueryID: q.id, - Stats: q.stats, - } - } - - switch tev := ev.(type) { - case *EventQueryCancel: - q.markFinished() - return &StateQueryFinished{ - QueryID: q.id, - Stats: q.stats, - } - case *EventQueryMessageResponse[K, A]: - q.onMessageResponse(ctx, tev.NodeID, tev.Response) - case *EventQueryMessageFailure[K]: - q.onMessageFailure(ctx, tev.NodeID) - case nil: - // TEMPORARY: no event to process - default: - panic(fmt.Sprintf("unexpected event: %T", tev)) - } - - // count number of successes in the order of the iteration - successes := 0 - - // progressing is set to true if any node is still awaiting contact - progressing := false - - // TODO: if stalled then we should contact all remaining nodes that have not already been queried - atCapacity := func() bool { - return q.inFlight >= q.cfg.Concurrency - } - - // get all the nodes in order of distance from the target - // TODO: turn this into a walk or iterator on trie.Trie - - var returnState QueryState - - q.iter.Each(ctx, func(ctx context.Context, ni *NodeStatus[K]) bool { - switch st := ni.State.(type) { - case *StateNodeWaiting: - if q.cfg.Clock.Now().After(st.Deadline) { - // mark node as unresponsive - ni.State = &StateNodeUnresponsive{} - q.inFlight-- - q.stats.Failure++ - } else if atCapacity() { - returnState = &StateQueryWaitingAtCapacity{ - QueryID: q.id, - Stats: q.stats, - } - return true - } else { - // The iterator is still waiting for a result from a node so can't be considered done - progressing = true - } - case *StateNodeSucceeded: - successes++ - // The iterator has attempted to contact all nodes closer than this one. - // If the iterator is not progressing then it doesn't expect any more nodes to be added to the list. - // If it has contacted at least NumResults nodes successfully then the iteration is done. - if !progressing && successes >= q.cfg.NumResults { - q.markFinished() - returnState = &StateQueryFinished{ - QueryID: q.id, - Stats: q.stats, - } - return true - } - - case *StateNodeNotContacted: - if !atCapacity() { - deadline := q.cfg.Clock.Now().Add(q.cfg.RequestTimeout) - ni.State = &StateNodeWaiting{Deadline: deadline} - q.inFlight++ - q.stats.Requests++ - if q.stats.Start.IsZero() { - q.stats.Start = q.cfg.Clock.Now() - } - returnState = &StateQueryWaitingMessage[K, A]{ - NodeID: ni.NodeID, - QueryID: q.id, - Stats: q.stats, - ProtocolID: q.protocolID, - Message: q.msg, - } - return true - - } - returnState = &StateQueryWaitingAtCapacity{ - QueryID: q.id, - Stats: q.stats, - } - return true - case *StateNodeUnresponsive: - // ignore - case *StateNodeFailed: - // ignore - default: - panic(fmt.Sprintf("unexpected state: %T", ni.State)) - } - - return false - }) - - if returnState != nil { - return returnState - } - - if q.inFlight > 0 { - // The iterator is still waiting for results and not at capacity - return &StateQueryWaitingWithCapacity{ - QueryID: q.id, - Stats: q.stats, - } - } - - // The iterator is finished because all available nodes have been contacted - // and the iterator is not waiting for any more results. - q.markFinished() - return &StateQueryFinished{ - QueryID: q.id, - Stats: q.stats, - } -} - -func (q *Query[K, A]) markFinished() { - q.finished = true - if q.stats.End.IsZero() { - q.stats.End = q.cfg.Clock.Now() - } -} - -// onMessageResponse processes the result of a successful response received from a node. -func (q *Query[K, A]) onMessageResponse(ctx context.Context, node kad.NodeID[K], resp kad.Response[K, A]) { - ni, found := q.iter.Find(node.Key()) - if !found { - // got a rogue message - return - } - switch st := ni.State.(type) { - case *StateNodeWaiting: - q.inFlight-- - q.stats.Success++ - case *StateNodeUnresponsive: - q.stats.Success++ - - case *StateNodeNotContacted: - // ignore duplicate or late response - return - case *StateNodeFailed: - // ignore duplicate or late response - return - case *StateNodeSucceeded: - // ignore duplicate or late response - return - default: - panic(fmt.Sprintf("unexpected state: %T", st)) - } - - if resp != nil { - // add closer nodes to list - for _, info := range resp.CloserNodes() { - // exclude self from closest nodes - if key.Equal(info.ID().Key(), q.self.Key()) { - continue - } - q.iter.Add(&NodeStatus[K]{ - NodeID: info.ID(), - State: &StateNodeNotContacted{}, - }) - } - } - ni.State = &StateNodeSucceeded{} -} - -// onMessageFailure processes the result of a failed attempt to contact a node. -func (q *Query[K, A]) onMessageFailure(ctx context.Context, node kad.NodeID[K]) { - ni, found := q.iter.Find(node.Key()) - if !found { - // got a rogue message - return - } - switch st := ni.State.(type) { - case *StateNodeWaiting: - q.inFlight-- - q.stats.Failure++ - case *StateNodeUnresponsive: - // update node state to failed - break - case *StateNodeNotContacted: - // update node state to failed - break - case *StateNodeFailed: - // ignore duplicate or late response - return - case *StateNodeSucceeded: - // ignore duplicate or late response - return - default: - panic(fmt.Sprintf("unexpected state: %T", st)) - } - - ni.State = &StateNodeFailed{} -} diff --git a/query/query_test.go b/query/query_test.go deleted file mode 100644 index 25760c1..0000000 --- a/query/query_test.go +++ /dev/null @@ -1,1119 +0,0 @@ -package query - -import ( - "context" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" -) - -func TestQueryConfigValidate(t *testing.T) { - t.Run("default is valid", func(t *testing.T) { - cfg := DefaultQueryConfig[key.Key8]() - require.NoError(t, cfg.Validate()) - }) - - t.Run("clock is not nil", func(t *testing.T) { - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = nil - require.Error(t, cfg.Validate()) - }) - - t.Run("request timeout positive", func(t *testing.T) { - cfg := DefaultQueryConfig[key.Key8]() - cfg.RequestTimeout = 0 - require.Error(t, cfg.Validate()) - cfg.RequestTimeout = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("concurrency positive", func(t *testing.T) { - cfg := DefaultQueryConfig[key.Key8]() - cfg.Concurrency = 0 - require.Error(t, cfg.Validate()) - cfg.Concurrency = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("num results positive", func(t *testing.T) { - cfg := DefaultQueryConfig[key.Key8]() - cfg.NumResults = 0 - require.Error(t, cfg.Validate()) - cfg.NumResults = -1 - require.Error(t, cfg.Validate()) - }) -} - -func TestQueryMessagesNode(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{a} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - - msg := kadtest.NewRequest("1", target) - - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is request to send a message to the node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // check that we are messaging the correct node with the right message - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, queryID, st.QueryID) - require.Equal(t, a, st.NodeID) - require.Equal(t, protocolID, st.ProtocolID) - require.Equal(t, msg, st.Message) - require.Equal(t, clk.Now(), st.Stats.Start) - require.Equal(t, 1, st.Stats.Requests) - require.Equal(t, 0, st.Stats.Success) - - // advancing now reports that the query is waiting for a response but its underlying query still has capacity - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingWithCapacity{}, state) - stw := state.(*StateQueryWaitingWithCapacity) - require.Equal(t, 1, stw.Stats.Requests) - require.Equal(t, 0, st.Stats.Success) -} - -func TestQueryMessagesNearest(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000011) - far := kadtest.NewID(key.Key8(0b11011011)) - near := kadtest.NewID(key.Key8(0b00000110)) - - // ensure near is nearer to target than far is - require.Less(t, target.Xor(near.Key()), target.Xor(far.Key())) - - // knownNodes are in "random" order with furthest before nearest - knownNodes := []kad.NodeID[key.Key8]{ - far, - near, - } - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - - msg := kadtest.NewRequest("1", target) - - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is message the nearest node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // check that we are contacting the nearest node first - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, near, st.NodeID) -} - -func TestQueryCancelFinishesQuery(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{a} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - - msg := kadtest.NewRequest("1", target) - - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is request to send a message to the node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - clk.Add(time.Second) - - // cancel the query - state = qry.Advance(ctx, &EventQueryCancel{}) - require.IsType(t, &StateQueryFinished{}, state) - - stf := state.(*StateQueryFinished) - require.Equal(t, 1, stf.Stats.Requests) - - // no successful responses were received before query was cancelled - require.Equal(t, 0, stf.Stats.Success) - - // no failed responses were received before query was cancelled - require.Equal(t, 0, stf.Stats.Failure) - - // query should have an end time - require.Equal(t, clk.Now(), stf.Stats.End) -} - -func TestQueryNoClosest(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000011) - - // no known nodes to start with - knownNodes := []kad.NodeID[key.Key8]{} - - iter := NewClosestNodesIter(target) - - clk := clock.NewMock() - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - - msg := kadtest.NewRequest("1", target) - - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // query is finished because there were no nodes to contat - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryFinished{}, state) - - stf := state.(*StateQueryFinished) - - // no requests were made - require.Equal(t, 0, stf.Stats.Requests) - - // no successful responses were received before query was cancelled - require.Equal(t, 0, stf.Stats.Success) - - // no failed responses were received before query was cancelled - require.Equal(t, 0, stf.Stats.Failure) - - // query should have an end time - require.Equal(t, clk.Now(), stf.Stats.End) -} - -func TestQueryWaitsAtCapacity(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{a, b, c} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = 2 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is request to send a message to the node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, a, st.NodeID) - require.Equal(t, 1, st.Stats.Requests) - - // advancing sends the message to the next node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) - require.Equal(t, 2, st.Stats.Requests) - - // advancing now reports that the query is waiting at capacity since there are 2 messages in flight - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingAtCapacity{}, state) - - stw := state.(*StateQueryWaitingAtCapacity) - require.Equal(t, 2, stw.Stats.Requests) -} - -func TestQueryTimedOutNodeMakesCapacity(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - // ensure the order of the known nodes - require.True(t, target.Xor(a.Key()).Compare(target.Xor(b.Key())) == -1) - require.True(t, target.Xor(b.Key()).Compare(target.Xor(c.Key())) == -1) - require.True(t, target.Xor(c.Key()).Compare(target.Xor(d.Key())) == -1) - - // knownNodes are in "random" order - knownNodes := []kad.NodeID[key.Key8]{b, c, a, d} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.RequestTimeout = 3 * time.Minute - cfg.Concurrency = len(knownNodes) - 1 // one less than the number of initial nodes - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the nearest node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, a, st.NodeID) - stwm := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, 1, stwm.Stats.Requests) - require.Equal(t, 0, stwm.Stats.Success) - require.Equal(t, 0, stwm.Stats.Failure) - - // advance time by one minute - clk.Add(time.Minute) - - // while the query has capacity the query should contact the next nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) - stwm = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, 2, stwm.Stats.Requests) - require.Equal(t, 0, stwm.Stats.Success) - require.Equal(t, 0, stwm.Stats.Failure) - - // advance time by one minute - clk.Add(time.Minute) - - // while the query has capacity the query should contact the second nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, c, st.NodeID) - stwm = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, 3, stwm.Stats.Requests) - require.Equal(t, 0, stwm.Stats.Success) - require.Equal(t, 0, stwm.Stats.Failure) - - // advance time by one minute - clk.Add(time.Minute) - - // the query should be at capacity - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingAtCapacity{}, state) - stwa := state.(*StateQueryWaitingAtCapacity) - require.Equal(t, 3, stwa.Stats.Requests) - require.Equal(t, 0, stwa.Stats.Success) - require.Equal(t, 0, stwa.Stats.Failure) - - // advance time by another minute, now at 4 minutes, first node connection attempt should now time out - clk.Add(time.Minute) - - // the first node request should have timed out, making capacity for the last node to attempt connection - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, d, st.NodeID) - - stwm = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, 4, stwm.Stats.Requests) - require.Equal(t, 0, stwm.Stats.Success) - require.Equal(t, 1, stwm.Stats.Failure) - - // advance time by another minute, now at 5 minutes, second node connection attempt should now time out - clk.Add(time.Minute) - - // advancing now makes more capacity - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingWithCapacity{}, state) - - stww := state.(*StateQueryWaitingWithCapacity) - require.Equal(t, 4, stww.Stats.Requests) - require.Equal(t, 0, stww.Stats.Success) - require.Equal(t, 2, stww.Stats.Failure) -} - -func TestQueryMessageResponseMakesCapacity(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - // ensure the order of the known nodes - require.True(t, target.Xor(a.Key()).Compare(target.Xor(b.Key())) == -1) - require.True(t, target.Xor(b.Key()).Compare(target.Xor(c.Key())) == -1) - require.True(t, target.Xor(c.Key()).Compare(target.Xor(d.Key())) == -1) - - // knownNodes are in "random" order - knownNodes := []kad.NodeID[key.Key8]{b, c, a, d} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = len(knownNodes) - 1 // one less than the number of initial nodes - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the nearest node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, a, st.NodeID) - stwm := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, 1, stwm.Stats.Requests) - require.Equal(t, 0, stwm.Stats.Success) - require.Equal(t, 0, stwm.Stats.Failure) - - // while the query has capacity the query should contact the next nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) - stwm = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, 2, stwm.Stats.Requests) - require.Equal(t, 0, stwm.Stats.Success) - require.Equal(t, 0, stwm.Stats.Failure) - - // while the query has capacity the query should contact the second nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, c, st.NodeID) - stwm = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, 3, stwm.Stats.Requests) - require.Equal(t, 0, stwm.Stats.Success) - require.Equal(t, 0, stwm.Stats.Failure) - - // the query should be at capacity - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingAtCapacity{}, state) - - // notify query that first node was contacted successfully, now node d can be contacted - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{NodeID: a}) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, d, st.NodeID) - stwm = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, 4, stwm.Stats.Requests) - require.Equal(t, 1, stwm.Stats.Success) - require.Equal(t, 0, stwm.Stats.Failure) - - // the query should be at capacity again - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingAtCapacity{}, state) - stwa := state.(*StateQueryWaitingAtCapacity) - require.Equal(t, 4, stwa.Stats.Requests) - require.Equal(t, 1, stwa.Stats.Success) - require.Equal(t, 0, stwa.Stats.Failure) -} - -func TestQueryCloserNodesAreAddedToIteration(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - // ensure the order of the known nodes - require.True(t, target.Xor(a.Key()).Compare(target.Xor(b.Key())) == -1) - require.True(t, target.Xor(b.Key()).Compare(target.Xor(c.Key())) == -1) - require.True(t, target.Xor(c.Key()).Compare(target.Xor(d.Key())) == -1) - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{d} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = 2 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the first node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, d, st.NodeID) - - // advancing reports query has capacity - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingWithCapacity{}, state) - - // notify query that first node was contacted successfully, with closer nodes - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: d, - Response: kadtest.NewResponse("resp_d", []kad.NodeInfo[key.Key8, kadtest.StrAddr]{ - kadtest.NewInfo(b, []kadtest.StrAddr{"addr_b"}), - kadtest.NewInfo(a, []kadtest.StrAddr{"addr_a"}), - }), - }) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // query should contact the next nearest uncontacted node - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, a, st.NodeID) -} - -func TestQueryCloserNodesIgnoresDuplicates(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - // ensure the order of the known nodes - require.True(t, target.Xor(a.Key()).Compare(target.Xor(b.Key())) == -1) - require.True(t, target.Xor(b.Key()).Compare(target.Xor(c.Key())) == -1) - require.True(t, target.Xor(c.Key()).Compare(target.Xor(d.Key())) == -1) - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{d, a} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = 2 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the first node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, a, st.NodeID) - - // next the query attempts to contact second nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, d, st.NodeID) - - // advancing reports query has no capacity - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingAtCapacity{}, state) - - // notify query that second node was contacted successfully, with closer nodes - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: d, - Response: kadtest.NewResponse("resp_d", []kad.NodeInfo[key.Key8, kadtest.StrAddr]{ - kadtest.NewInfo(b, []kadtest.StrAddr{"addr_b"}), - kadtest.NewInfo(a, []kadtest.StrAddr{"addr_a"}), - }), - }) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // query should contact the next nearest uncontacted node, which is b - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) -} - -func TestQueryCancelFinishesIteration(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{a} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = 2 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the first node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, a, st.NodeID) - - // cancel the query so it is now finished - state = qry.Advance(ctx, &EventQueryCancel{}) - require.IsType(t, &StateQueryFinished{}, state) - - stf := state.(*StateQueryFinished) - require.Equal(t, 0, stf.Stats.Success) -} - -func TestQueryFinishedIgnoresLaterEvents(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{b} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = 2 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the first node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) - - // cancel the query so it is now finished - state = qry.Advance(ctx, &EventQueryCancel{}) - require.IsType(t, &StateQueryFinished{}, state) - - // no successes - stf := state.(*StateQueryFinished) - require.Equal(t, 1, stf.Stats.Requests) - require.Equal(t, 0, stf.Stats.Success) - require.Equal(t, 0, stf.Stats.Failure) - - // notify query that second node was contacted successfully, with closer nodes - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: b, - Response: kadtest.NewResponse("resp_b", []kad.NodeInfo[key.Key8, kadtest.StrAddr]{ - kadtest.NewInfo(a, []kadtest.StrAddr{"addr_a"}), - }), - }) - - // query remains finished - require.IsType(t, &StateQueryFinished{}, state) - - // still no successes since contact message was after query had been cancelled - stf = state.(*StateQueryFinished) - require.Equal(t, 1, stf.Stats.Requests) - require.Equal(t, 0, stf.Stats.Success) - require.Equal(t, 0, stf.Stats.Failure) -} - -func TestQueryWithCloserIterIgnoresMessagesFromUnknownNodes(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{c} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = 2 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the first node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, c, st.NodeID) - stwm := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, 1, stwm.Stats.Requests) - require.Equal(t, 0, stwm.Stats.Success) - require.Equal(t, 0, stwm.Stats.Failure) - - // notify query that second node was contacted successfully, with closer nodes - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: b, - Response: kadtest.NewResponse("resp_b", []kad.NodeInfo[key.Key8, kadtest.StrAddr]{ - kadtest.NewInfo(a, []kadtest.StrAddr{"addr_a"}), - }), - }) - - // query ignores message from unknown node - require.IsType(t, &StateQueryWaitingWithCapacity{}, state) - - stwc := state.(*StateQueryWaitingWithCapacity) - require.Equal(t, 1, stwc.Stats.Requests) - require.Equal(t, 0, stwc.Stats.Success) - require.Equal(t, 0, stwc.Stats.Failure) -} - -func TestQueryWithCloserIterFinishesWhenNumResultsReached(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{a, b, c, d} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = 4 - cfg.NumResults = 2 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // contact first node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, a, st.NodeID) - - // contact second node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) - - // notify query that first node was contacted successfully - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: a, - }) - - // query attempts to contact third node - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, c, st.NodeID) - - // notify query that second node was contacted successfully - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: b, - }) - - // query has finished since it contacted the NumResults closest nodes - require.IsType(t, &StateQueryFinished{}, state) -} - -func TestQueryWithCloserIterContinuesUntilNumResultsReached(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - - // one known node to start with, the furthesr - knownNodes := []kad.NodeID[key.Key8]{c} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = 4 - cfg.NumResults = 2 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // contact first node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, c, st.NodeID) - - // notify query that node was contacted successfully and tell it about - // a closer one - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: c, - Response: kadtest.NewResponse("resp_c", []kad.NodeInfo[key.Key8, kadtest.StrAddr]{ - kadtest.NewInfo(b, []kadtest.StrAddr{"addr_b"}), - }), - }) - - // query attempts to contact second node - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) - - // notify query that node was contacted successfully and tell it about - // a closer one - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: b, - Response: kadtest.NewResponse("resp_b", []kad.NodeInfo[key.Key8, kadtest.StrAddr]{ - kadtest.NewInfo(a, []kadtest.StrAddr{"addr_a"}), - }), - }) - - // query has seen enough successful contacts but there are still - // closer nodes that have not been contacted, so query attempts - // to contact third node - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, a, st.NodeID) - - // notify query that second node was contacted successfully - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: a, - }) - - // query has finished since it contacted the NumResults closest nodes - require.IsType(t, &StateQueryFinished{}, state) - - stf := state.(*StateQueryFinished) - require.Equal(t, 3, stf.Stats.Success) -} - -func TestQueryNotContactedMakesCapacity(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - // ensure the order of the known nodes - require.True(t, target.Xor(a.Key()).Compare(target.Xor(b.Key())) == -1) - require.True(t, target.Xor(b.Key()).Compare(target.Xor(c.Key())) == -1) - require.True(t, target.Xor(c.Key()).Compare(target.Xor(d.Key())) == -1) - - knownNodes := []kad.NodeID[key.Key8]{a, b, c, d} - iter := NewSequentialIter[key.Key8]() - - clk := clock.NewMock() - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = len(knownNodes) - 1 // one less than the number of initial nodes - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the nearest node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, a, st.NodeID) - - // while the query has capacity the query should contact the next nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) - - // while the query has capacity the query should contact the second nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, c, st.NodeID) - - // the query should be at capacity - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingAtCapacity{}, state) - - // notify query that first node was not contacted, now node d can be contacted - state = qry.Advance(ctx, &EventQueryMessageFailure[key.Key8]{NodeID: a}) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, d, st.NodeID) - - // the query should be at capacity again - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingAtCapacity{}, state) -} - -func TestQueryAllNotContactedFinishes(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - - // knownNodes are in "random" order - knownNodes := []kad.NodeID[key.Key8]{a, b, c} - - clk := clock.NewMock() - - iter := NewSequentialIter[key.Key8]() - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = len(knownNodes) // allow all to be contacted at once - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the nearest node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // while the query has capacity the query should contact the next nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // while the query has capacity the query should contact the third nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // the query should be at capacity - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingAtCapacity{}, state) - - // notify query that first node was not contacted - state = qry.Advance(ctx, &EventQueryMessageFailure[key.Key8]{NodeID: a}) - require.IsType(t, &StateQueryWaitingWithCapacity{}, state) - - // notify query that second node was not contacted - state = qry.Advance(ctx, &EventQueryMessageFailure[key.Key8]{NodeID: b}) - require.IsType(t, &StateQueryWaitingWithCapacity{}, state) - - // notify query that third node was not contacted - state = qry.Advance(ctx, &EventQueryMessageFailure[key.Key8]{NodeID: c}) - - // query has finished since it contacted all possible nodes - require.IsType(t, &StateQueryFinished{}, state) - - stf := state.(*StateQueryFinished) - require.Equal(t, 0, stf.Stats.Success) -} - -func TestQueryAllContactedFinishes(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - - knownNodes := []kad.NodeID[key.Key8]{a, b, c} - - clk := clock.NewMock() - - iter := NewSequentialIter[key.Key8]() - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = len(knownNodes) // allow all to be contacted at once - cfg.NumResults = len(knownNodes) + 1 // one more than the size of the network - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := kadtest.NewID(key.Key8(0)) - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the nearest node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // while the query has capacity the query should contact the next nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // while the query has capacity the query should contact the third nearest node - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - - // the query should be at capacity - state = qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingAtCapacity{}, state) - - // notify query that first node was contacted successfully, but no closer nodes - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{NodeID: a}) - require.IsType(t, &StateQueryWaitingWithCapacity{}, state) - - // notify query that second node was contacted successfully, but no closer nodes - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{NodeID: b}) - require.IsType(t, &StateQueryWaitingWithCapacity{}, state) - - // notify query that third node was contacted successfully, but no closer nodes - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{NodeID: c}) - - // query has finished since it contacted all possible nodes, even though it didn't - // reach the desired NumResults - require.IsType(t, &StateQueryFinished{}, state) - - stf := state.(*StateQueryFinished) - require.Equal(t, 3, stf.Stats.Success) -} - -func TestQueryNeverMessagesSelf(t *testing.T) { - ctx := context.Background() - - target := key.Key8(0b00000001) - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - - // one known node to start with - knownNodes := []kad.NodeID[key.Key8]{b} - - clk := clock.NewMock() - - iter := NewClosestNodesIter(target) - - cfg := DefaultQueryConfig[key.Key8]() - cfg.Clock = clk - cfg.Concurrency = 2 - - msg := kadtest.NewRequest("1", target) - queryID := QueryID("test") - protocolID := address.ProtocolID("testprotocol") - - self := a - qry, err := NewQuery[key.Key8, kadtest.StrAddr](self, queryID, protocolID, msg, iter, knownNodes, cfg) - require.NoError(t, err) - - // first thing the new query should do is contact the first node - state := qry.Advance(ctx, nil) - require.IsType(t, &StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateQueryWaitingMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) - - // notify query that first node was contacted successfully, with closer nodes - state = qry.Advance(ctx, &EventQueryMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: b, - Response: kadtest.NewResponse("resp_b", []kad.NodeInfo[key.Key8, kadtest.StrAddr]{ - kadtest.NewInfo(a, []kadtest.StrAddr{"addr_a"}), - }), - }) - - // query is finished since it can't contact self - require.IsType(t, &StateQueryFinished{}, state) - - // one successful message - stf := state.(*StateQueryFinished) - require.Equal(t, 1, stf.Stats.Requests) - require.Equal(t, 1, stf.Stats.Success) - require.Equal(t, 0, stf.Stats.Failure) -} diff --git a/query/simplequery/README.md b/query/simplequery/README.md deleted file mode 100644 index a4f0cb4..0000000 --- a/query/simplequery/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Simple Query - -Author: [Guillaume Michel](https://github.com/guillaumemichel) - -`SimpleQuery` is a simple multi threaded single worker query mechanism. - -It takes as input the target Kademlia Key, the `ProtocolID` of the request, the request message, an empty response message (to parse the reponse once received), a concurrency paramter, a default message timeout value, the message endpoint used to communicate with remote peers, a routing table to select the closest peers and add newly discovered peers, a scheduler to give actions to the single worker, and a `handleResultFn` function that is defined by the called. - -The `handleResultFn` is used by the caller to interact with the received responses, and to save a state for the query. It is through this function that the caller decides when the query terminates. - -```go -func NewSimpleQuery(ctx context.Context, kadid key.KadKey, proto address.ProtocolID, - req message.MinKadMessage, resp message.MinKadResponseMessage, concurrency int, - timeout time.Duration, msgEndpoint endpoint.Endpoint, rt routingtable.RoutingTable, - sched scheduler.Scheduler, handleResultFn HandleResultFn) *SimpleQuery -``` - -### Query setup - -![alt text](../../design/excalidraw/query-setup.png) - -### Query execution - -![alt text](../../design/excalidraw/query-run.png) - diff --git a/query/simplequery/options.go b/query/simplequery/options.go deleted file mode 100644 index a86998c..0000000 --- a/query/simplequery/options.go +++ /dev/null @@ -1,183 +0,0 @@ -package simplequery - -import ( - "context" - "fmt" - "time" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" -) - -// Config is a structure containing all the options that can be used when -// constructing a SimpleQuery. -type Config[K kad.Key[K], A kad.Address[A]] struct { - // ProtocolID is the protocol identifier used to send the request - ProtocolID address.ProtocolID - // NumberUsefulCloserPeers is the number of closer peers to look for in the - // provided routing table when starting the request - NumberUsefulCloserPeers int - // Concurrency is the maximal number of simultaneous inflight requests for - // this query - Concurrency int - - // RequestTimeout is the timeout value for a single request - RequestTimeout time.Duration - // PeerstoreTTL is the TTL value for newly discovered peers in the peerstore - PeerstoreTTL time.Duration - - // HandleResultFn is a function that is called when a response is received - // for a request. It is used to determine whether the query should be - // stopped and whether the peerlist should be updated. - HandleResultsFunc HandleResultFn[K, A] - // NotifyFailureFn is a function that is called when the query fails. It is - // used to notify the user that the query failed. - NotifyFailureFunc NotifyFailureFn - - // RoutingTable is the routing table used to find closer peers. It is - // updated with newly discovered peers. - RoutingTable kad.RoutingTable[K, kad.NodeID[K]] - // Endpoint is the message endpoint used to send requests - Endpoint endpoint.Endpoint[K, A] - // Scheduler is the scheduler used to schedule events for the single worker - Scheduler event.Scheduler -} - -// Apply applies the SimpleQuery options to this Option -func (cfg *Config[K, A]) Apply(opts ...Option[K, A]) error { - for i, opt := range opts { - if err := opt(cfg); err != nil { - return fmt.Errorf("SimpleQuery option %d failed: %s", i, err) - } - } - if cfg.RoutingTable == nil { - return fmt.Errorf("SimpleQuery option RoutingTable cannot be nil") - } - if cfg.Endpoint == nil { - return fmt.Errorf("SimpleQuery option Endpoint cannot be nil") - } - if cfg.Scheduler == nil { - return fmt.Errorf("SimpleQuery option Scheduler cannot be nil") - } - return nil -} - -// Option type for SimpleQuery -type Option[K kad.Key[K], A kad.Address[A]] func(*Config[K, A]) error - -// DefaultConfig is the default options for SimpleQuery. This option is always -// prepended to the list of options passed to the SimpleQuery constructor. -// Note that most of the fields are left empty, and must be filled by the user. -func DefaultConfig[K kad.Key[K], A kad.Address[A]](cfg *Config[K, A]) error { - cfg.NumberUsefulCloserPeers = 20 - cfg.Concurrency = 10 - - cfg.RequestTimeout = time.Second - cfg.PeerstoreTTL = 30 * time.Minute - - cfg.HandleResultsFunc = func(ctx context.Context, id kad.NodeID[K], - resp kad.Response[K, A], - ) (bool, []kad.NodeID[K]) { - ids := make([]kad.NodeID[K], len(resp.CloserNodes())) - for i, n := range resp.CloserNodes() { - ids[i] = n.ID() - } - return false, ids - } - cfg.NotifyFailureFunc = func(context.Context) {} - - return nil -} - -func WithProtocolID[K kad.Key[K], A kad.Address[A]](pid address.ProtocolID) Option[K, A] { - return func(cfg *Config[K, A]) error { - cfg.ProtocolID = pid - return nil - } -} - -func WithNumberUsefulCloserPeers[K kad.Key[K], A kad.Address[A]](n int) Option[K, A] { - return func(cfg *Config[K, A]) error { - if n <= 0 { - return fmt.Errorf("NumberUsefulCloserPeers must be positive") - } - cfg.NumberUsefulCloserPeers = n - return nil - } -} - -func WithConcurrency[K kad.Key[K], A kad.Address[A]](n int) Option[K, A] { - return func(cfg *Config[K, A]) error { - if n <= 0 { - return fmt.Errorf("concurrency parameter must be positive") - } - cfg.Concurrency = n - return nil - } -} - -func WithRequestTimeout[K kad.Key[K], A kad.Address[A]](timeout time.Duration) Option[K, A] { - return func(cfg *Config[K, A]) error { - cfg.RequestTimeout = timeout - return nil - } -} - -func WithPeerstoreTTL[K kad.Key[K], A kad.Address[A]](ttl time.Duration) Option[K, A] { - return func(cfg *Config[K, A]) error { - cfg.PeerstoreTTL = ttl - return nil - } -} - -func WithHandleResultsFunc[K kad.Key[K], A kad.Address[A]](fn HandleResultFn[K, A]) Option[K, A] { - return func(cfg *Config[K, A]) error { - if fn == nil { - return fmt.Errorf("HandleResultsFunc cannot be nil") - } - cfg.HandleResultsFunc = fn - return nil - } -} - -func WithNotifyFailureFunc[K kad.Key[K], A kad.Address[A]](fn NotifyFailureFn) Option[K, A] { - return func(cfg *Config[K, A]) error { - if fn == nil { - return fmt.Errorf("NotifyFailureFunc cannot be nil") - } - cfg.NotifyFailureFunc = fn - return nil - } -} - -func WithRoutingTable[K kad.Key[K], A kad.Address[A]](rt kad.RoutingTable[K, kad.NodeID[K]]) Option[K, A] { - return func(cfg *Config[K, A]) error { - if rt == nil { - return fmt.Errorf("SimpleQuery option RoutingTable cannot be nil") - } - cfg.RoutingTable = rt - return nil - } -} - -func WithEndpoint[K kad.Key[K], A kad.Address[A]](ep endpoint.Endpoint[K, A]) Option[K, A] { - return func(cfg *Config[K, A]) error { - if ep == nil { - return fmt.Errorf("SimpleQuery option Endpoint cannot be nil") - } - cfg.Endpoint = ep - return nil - } -} - -func WithScheduler[K kad.Key[K], A kad.Address[A]](sched event.Scheduler) Option[K, A] { - return func(cfg *Config[K, A]) error { - if sched == nil { - return fmt.Errorf("SimpleQuery option Scheduler cannot be nil") - } - cfg.Scheduler = sched - return nil - } -} diff --git a/query/simplequery/peerlist.go b/query/simplequery/peerlist.go deleted file mode 100644 index c8cdce5..0000000 --- a/query/simplequery/peerlist.go +++ /dev/null @@ -1,299 +0,0 @@ -package simplequery - -import ( - "sort" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/endpoint" -) - -type NodeStatus uint8 - -const ( - queued NodeStatus = iota - waiting - queried - unreachable -) - -type nodeInfo[K kad.Key[K], A kad.Address[A]] struct { - distance K - status NodeStatus - id kad.NodeID[K] - addrs []A - tryAgainOnFailure bool - - next *nodeInfo[K, A] -} - -type PeerList[K kad.Key[K], A kad.Address[A]] struct { - target K - endpoint endpoint.NetworkedEndpoint[K, A] - - closest *nodeInfo[K, A] - closestQueued *nodeInfo[K, A] - - queuedCount int -} - -func newPeerList[K kad.Key[K], A kad.Address[A]](target K, ep endpoint.Endpoint[K, A]) *PeerList[K, A] { - nep, ok := ep.(endpoint.NetworkedEndpoint[K, A]) - if !ok { - nep = nil - } - return &PeerList[K, A]{ - target: target, - endpoint: nep, - } -} - -// normally peers should already be ordered with distance to target, but we -// sort them just in case -func (pl *PeerList[K, A]) addToPeerlist(ids []kad.NodeID[K]) { - // linked list of new peers sorted by distance to target - newHead := sliceToPeerInfos[K, A](pl.target, ids) - if newHead == nil { - return - } - - oldHead := pl.closest - - // if the list is empty, define first new peer as closest - if oldHead == nil { - pl.closest = newHead - pl.closestQueued = newHead - - for curr := newHead; curr != nil; curr = curr.next { - pl.queuedCount++ - } - return - } - - // merge the new sorted list into the existing sorted list - var prev *nodeInfo[K, A] - currOld := true // current element is from old list - closestQueuedReached := false - - r := oldHead.distance.Compare(newHead.distance) - if r > 0 { - pl.closest = newHead - pl.closestQueued = newHead - currOld = false - } - - for { - if r > 0 { - // newHead is closer than oldHead - - if !closestQueuedReached { - // newHead is closer than closestQueued, update closestQueued - pl.closestQueued = newHead - closestQueuedReached = true - } - if currOld && prev != nil { - prev.next = newHead - currOld = false - } - prev = newHead - newHead = newHead.next - - // increased queued count as all new peers are queued - pl.queuedCount++ - } else { - // oldHead is closer than newHead - - if !closestQueuedReached && oldHead == pl.closestQueued { - // old closestQueued is closer than newHead, - // don't update closestQueued - closestQueuedReached = true - } - - if !currOld && prev != nil { - prev.next = oldHead - currOld = true - } - if r == 0 { - // newHead is a duplicate of oldHead, discard newHead - newHead = newHead.next - - if pl.endpoint != nil && (oldHead.status == unreachable || - oldHead.status == waiting) { - // If oldHead is unreachable, and new addresses are discovered - // for this NodeID, set status to queued as we will try again. - // If oldHead is waiting, set tryAgainOnFailure to true - // so that we will try again with the newly discovered - // addresses upon failure. - na, err := pl.endpoint.NetworkAddress(oldHead.id) - if err == nil { - for _, addr := range na.Addresses() { - found := false - for _, oldAddr := range oldHead.addrs { - if addr.Equal(oldAddr) { - found = true - break - } - } - if !found { - if oldHead.status == unreachable { - pl.enqueueUnreachablePeer(oldHead) - } else if oldHead.status == waiting { - oldHead.tryAgainOnFailure = true - } - } - } - } - } - } - prev = oldHead - oldHead = oldHead.next - } - // we are done when we reach the end of either list - if oldHead == nil || newHead == nil { - break - } - r = oldHead.distance.Compare(newHead.distance) - } - - // append the remaining list to the end - if oldHead == nil { - if !closestQueuedReached { - pl.closestQueued = newHead - } - prev.next = newHead - - // if there are still new peers to be appended, increase queued count - for curr := newHead; curr != nil; curr = curr.next { - pl.queuedCount++ - } - } else { - prev.next = oldHead - } -} - -func sliceToPeerInfos[K kad.Key[K], A kad.Address[A]](target K, ids []kad.NodeID[K]) *nodeInfo[K, A] { - // create a new list of nodeInfo - newPeers := make([]*nodeInfo[K, A], 0, len(ids)) - for _, id := range ids { - newPeer := addrInfoToPeerInfo[K, A](target, id) - if newPeer != nil { - newPeers = append(newPeers, newPeer) - } - } - - if len(newPeers) == 0 { - return nil - } - - // sort the new list - sort.Slice(newPeers, func(i, j int) bool { - return newPeers[i].distance.Compare(newPeers[j].distance) < 0 - }) - - // convert slice to linked list and remove duplicates - curr := newPeers[0] - for i := 1; i < len(newPeers); i++ { - if !key.Equal(curr.distance, newPeers[i].distance) { - curr.next = newPeers[i] - curr = curr.next - } - } - // return head of linked list - return newPeers[0] -} - -func addrInfoToPeerInfo[K kad.Key[K], A kad.Address[A]](target K, id kad.NodeID[K]) *nodeInfo[K, A] { - if id == nil || id.String() == "" || target.BitLen() != id.Key().BitLen() { - return nil - } - return &nodeInfo[K, A]{ - distance: target.Xor(id.Key()), - status: queued, - id: id, - } -} - -func (pl *PeerList[K, A]) enqueueUnreachablePeer(pi *nodeInfo[K, A]) { - if pi != nil { - // curr is the id we are looking for - if pi.status != queued { - pi.tryAgainOnFailure = false - pi.status = queued - - pl.queuedCount++ - // if curr is closer to target than closestQueued, update closestQueued - if pi.distance.Compare(pl.closestQueued.distance) < 0 { - pl.closestQueued = pi - } - } - } -} - -// setPeerWaiting sets the status of a "queued" peer to "waiting". it records -// the addresses associated with id in the peerstore at the time of the request -// to curr.addrs. -func (pl *PeerList[K, A]) popClosestQueued() kad.NodeID[K] { - if pl.closestQueued == nil { - return nil - } - pi := pl.closestQueued - if pl.endpoint != nil { - na, _ := pl.endpoint.NetworkAddress(pi.id) - for na == nil { - // if peer doesn't have addresses, set status to unreachable - pi.status = unreachable - pl.queuedCount-- - - pi = findNextQueued(pi) - if pi == nil { - return nil - } - na, _ = pl.endpoint.NetworkAddress(pi.id) - } - pi.addrs = na.Addresses() - } - pi.status = waiting - pl.queuedCount-- - - pl.closestQueued = findNextQueued(pi) - return pi.id -} - -func (pl *PeerList[K, A]) queriedPeer(id kad.NodeID[K]) { - curr := pl.closest - for curr != nil && curr.id.String() != id.String() { - curr = curr.next - } - if curr != nil { - // curr is the id we are looking for - if curr.status == waiting { - curr.status = queried - } - } -} - -// unreachablePeer sets the status of a "waiting" peer to "unreachable". -func (pl *PeerList[K, A]) unreachablePeer(id kad.NodeID[K]) { - curr := pl.closest - for curr != nil && curr.id.String() != id.String() { - curr = curr.next - } - if curr != nil { - // curr is the id we are looking for - if curr.status == waiting { - if curr.tryAgainOnFailure { - pl.enqueueUnreachablePeer(curr) - } else { - curr.status = unreachable - } - } - } -} - -func findNextQueued[K kad.Key[K], A kad.Address[A]](pi *nodeInfo[K, A]) *nodeInfo[K, A] { - curr := pi - for curr != nil && curr.status != queued { - curr = curr.next - } - return curr -} diff --git a/query/simplequery/peerlist_test.go b/query/simplequery/peerlist_test.go deleted file mode 100644 index 820d466..0000000 --- a/query/simplequery/peerlist_test.go +++ /dev/null @@ -1,293 +0,0 @@ -package simplequery - -import ( - "context" - "net" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/sim" -) - -func TestAddPeers(t *testing.T) { - target := kadtest.NewID(key.Key8(0x00)) - - // create empty peer list - pl := newPeerList[key.Key8, net.IP](target.Key(), nil) - - require.Nil(t, pl.closest) - require.Nil(t, pl.closestQueued) - - initialIds := []kad.NodeID[key.Key8]{ - kadtest.NewID(key.Key8(0xa0)), - kadtest.NewID(key.Key8(0x08)), - kadtest.NewID(key.Key8(0x20)), - kadtest.NewID(key.Key8(0xa0)), // duplicate with initialIds[0] - } - - // add 4 peers (incl. 1 duplicate) - pl.addToPeerlist(initialIds) - require.Equal(t, len(initialIds)-1, pl.queuedCount) - - curr := pl.closest - // verify that closest peer is initialIds[1] - require.Equal(t, initialIds[1], curr.id) - // verify that closestQueued peer is initialIds[1] - require.Equal(t, initialIds[1], pl.closestQueued.id) - curr = curr.next - // verify that next peer is initialIds[2] - require.Equal(t, initialIds[2], curr.id) - curr = curr.next - // verify that next peer is initialIds[0] - require.Equal(t, initialIds[0], curr.id) - // end of the list - require.Nil(t, curr.next) - - additionalIds := []kad.NodeID[key.Key8]{ - kadtest.NewID(key.Key8(0x40)), - kadtest.NewID(key.Key8(0x20)), // duplicate with initialIds[2] - kadtest.NewID(key.Key8(0x60)), - kadtest.NewID(key.Key8(0x80)), - kadtest.NewID(key.Key8(0x60)), // duplicate additionalIds[2] - kadtest.NewID(key.Key8(0x18)), - kadtest.NewID(key.Key8(0xf0)), - } - - // add 7 more peers (incl. 2 duplicates) - pl.addToPeerlist(additionalIds) - require.Equal(t, len(initialIds)-1+len(additionalIds)-2, pl.queuedCount) - - curr = pl.closest - // verify that closest peer is initialIds[1] 0x08 - require.Equal(t, initialIds[1], curr.id) - // verify that closestQueued peer is initialIds[1] 0x08 - require.Equal(t, initialIds[1], pl.closestQueued.id) - curr = curr.next - // verify that next peer is additionalIds[5] 0x18 - require.Equal(t, additionalIds[5], curr.id) - curr = curr.next - // verify that next peer is initialIds[2] 0x20 - require.Equal(t, initialIds[2], curr.id) - curr = curr.next - // verify that next peer is additionalIds[0] 0x40 - require.Equal(t, additionalIds[0], curr.id) - curr = curr.next - // verify that next peer is additionalIds[2] 0x60 - require.Equal(t, additionalIds[2], curr.id) - curr = curr.next - // verify that next peer is additionalIds[3] 0x80 - require.Equal(t, additionalIds[3], curr.id) - curr = curr.next - // verify that next peer is initialIds[0] 0xa0 - require.Equal(t, initialIds[0], curr.id) - curr = curr.next - // verify that next peer is additionalIds[6] 0xf0 - require.Equal(t, additionalIds[6], curr.id) - // end of the list - require.Nil(t, curr.next) - - // add 1 more peer, the closest to target - newId := kadtest.NewID(key.Key8(0x00)) - pl.addToPeerlist([]kad.NodeID[key.Key8]{newId}) - require.Equal(t, len(initialIds)-1+len(additionalIds)-2+1, pl.queuedCount) - - // it must be the closest peer - require.Equal(t, newId, pl.closest.id) - require.Equal(t, newId, pl.closestQueued.id) - - // add empty list - pl.addToPeerlist([]kad.NodeID[key.Key8]{}) - require.Equal(t, len(initialIds)-1+len(additionalIds)-2+1, pl.queuedCount) - - // add list containing nil element - pl.addToPeerlist([]kad.NodeID[key.Key8]{nil}) - require.Equal(t, len(initialIds)-1+len(additionalIds)-2+1, pl.queuedCount) -} - -func TestChangeStatus(t *testing.T) { - target := kadtest.NewID(key.Key8(0x00)) - - // create empty peer list - pl := newPeerList[key.Key8, net.IP](target.Key(), nil) - - require.Nil(t, pl.closest) - require.Nil(t, pl.closestQueued) - require.Nil(t, pl.popClosestQueued()) - - nPeers := 32 - ids := make([]kad.NodeID[key.Key8], nPeers) - for i := 0; i < nPeers; i++ { - ids[i] = kadtest.NewID(key.Key8(uint8(4 * i))) - } - - // add peers - pl.addToPeerlist(ids) - require.Equal(t, nPeers, pl.queuedCount) - - require.Equal(t, ids[0], pl.closest.id) - require.Equal(t, ids[0], pl.closestQueued.id) - - var popCounter int - // change status of the closest peer - require.Equal(t, ids[popCounter], pl.popClosestQueued()) - popCounter++ - // ids[0] is still the closest - require.Equal(t, ids[0], pl.closest.id) - // ids[1] is now the closestQueued - require.Equal(t, ids[popCounter], pl.closestQueued.id) - require.Equal(t, ids[popCounter], pl.popClosestQueued()) - popCounter++ - // ids[2] is now the closestQueued - require.Equal(t, ids[popCounter], pl.closestQueued.id) - - // mark ids[1] as queried - pl.queriedPeer(ids[1]) - require.Equal(t, waiting, pl.closest.status) - require.Equal(t, queried, pl.closest.next.status) - - // ids[2] cannot be set as unreachable (hasn't been waiting yet) - pl.unreachablePeer(ids[2]) - require.Equal(t, queued, pl.closest.next.next.status) - // set ids[2] as waiting - require.Equal(t, ids[popCounter], pl.popClosestQueued()) - popCounter++ - require.Equal(t, waiting, pl.closest.next.next.status) - // set ids[2] as unreachable - pl.unreachablePeer(ids[2]) - require.Equal(t, unreachable, pl.closest.next.next.status) - - // inserting a new peer that isn't the absolute closest, but is now the - // closest queued peer - newID := kadtest.NewID(key.Key8(0x05)) - pl.addToPeerlist([]kad.NodeID[key.Key8]{newID}) - require.Equal(t, newID, pl.closestQueued.id) - require.Equal(t, ids[0], pl.closest.id) -} - -func TestMultiAddrs(t *testing.T) { - ctx := context.Background() - self := kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0x80})) - ep := sim.NewEndpoint[key.Key256, net.IP](self, nil, nil) - - // create empty peer list - pl := newPeerList[key.Key256, net.IP](key.ZeroKey256(), ep) - - require.Nil(t, pl.closest) - require.Nil(t, pl.closestQueued) - - // create initial peers - nPeers := 5 - ids := make([]kad.NodeID[key.Key256], nPeers) - addrs := make([]*kadtest.Info[key.Key256, net.IP], nPeers) - for i := 0; i < nPeers; i++ { - id := kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{byte(16 * i)})) - ids[i] = id - addrs[i] = kadtest.NewInfo[key.Key256, net.IP](id, []net.IP{}) - ep.MaybeAddToPeerstore(ctx, addrs[i], 1) - } - - // ids[0]: 0000... - // ids[1]: 1000... - // ids[2]: 2000... - // etc. - - // add peers - pl.addToPeerlist(ids) - require.Equal(t, nPeers, pl.queuedCount) - - var popCounter int - // change status of the closest peer - require.Equal(t, ids[popCounter], pl.popClosestQueued()) - popCounter++ - require.Equal(t, ids[popCounter], pl.popClosestQueued()) - popCounter++ - - // change ids[1] from waiting to unreachable - require.Equal(t, waiting, pl.closest.next.status) - pl.unreachablePeer(ids[1]) - require.Equal(t, unreachable, pl.closest.next.status) - - // ids[1] is added again, with the same address, it should be ignored - pl.addToPeerlist([]kad.NodeID[key.Key256]{ids[1]}) - require.Equal(t, unreachable, pl.closest.next.status) - - // ids[1] added again, with a different address, its status should be queued - addrs[1].AddAddr(net.ParseIP("127.0.0.1")) - ep.MaybeAddToPeerstore(ctx, addrs[1], 1) - pl.addToPeerlist([]kad.NodeID[key.Key256]{ids[1]}) - require.Equal(t, queued, pl.closest.next.status) - - // change ids[1] to waiting again - require.Equal(t, ids[1], pl.popClosestQueued()) - - // mark ids[1] as unreachable again - pl.unreachablePeer(ids[1]) - require.Equal(t, unreachable, pl.closest.next.status) - - // ids[1] added again with same address as before, should remain unreachable - pl.addToPeerlist([]kad.NodeID[key.Key256]{ids[1]}) - require.Equal(t, unreachable, pl.closest.next.status) - - // add new addr to ids[1] - addrs[1].AddAddr(net.ParseIP("127.0.0.2")) - ep.MaybeAddToPeerstore(ctx, addrs[1], 1) - // add ids[1] again to peerlist, this time it should be queued - pl.addToPeerlist([]kad.NodeID[key.Key256]{ids[1]}) - require.Equal(t, queued, pl.closest.next.status) - require.Equal(t, ids[1], pl.popClosestQueued()) - - // ids[0] is still waiting - addrs[0].AddAddr(net.ParseIP("127.0.0.1")) - ep.MaybeAddToPeerstore(ctx, addrs[0], 1) - // add ids[0] new address to peerlist - pl.addToPeerlist([]kad.NodeID[key.Key256]{ids[0]}) - require.Equal(t, waiting, pl.closest.status) - require.True(t, pl.closest.tryAgainOnFailure) - - // ids[0] is now unreachable (on the tried address) - pl.unreachablePeer(ids[0]) - require.Equal(t, queued, pl.closest.status) - - // set ids[0] as waiting again - require.Equal(t, ids[0], pl.popClosestQueued()) - // set ids[2] as waiting - require.Equal(t, ids[popCounter], pl.popClosestQueued()) - popCounter++ // popCounter = 3 - require.Equal(t, waiting, pl.closest.next.next.status) - require.Equal(t, ids[popCounter], pl.popClosestQueued()) - popCounter++ // popCounter = 4 - - pid := kadtest.NewStringID("1D3oooUnknownPeer") - - // add unknown peer, this peer doesn't have any address (and the peerlist - // has an endpoint configured). Hence, it can never be queried, when - // "popped" it should be set as unreachable - pl.addToPeerlist([]kad.NodeID[key.Key256]{pid}) - require.Equal(t, nPeers-popCounter+1, pl.queuedCount) // 4 peers have been poped so far - require.Equal(t, pid, pl.closestQueued.id) - - // popClosestQueued should set pid as unreachable and return ids[4] - require.Equal(t, ids[popCounter], pl.popClosestQueued()) - popCounter++ // popCounter = 5 - // element between ids[3] and ids[4], that is pid - assert.Equal(t, unreachable, pl.closest.next.next.next.status) // TODO: might miss one .next (just removed it to pass the test) - assert.Equal(t, waiting, pl.closest.next.next.next.next.next.status) // ids[4] - assert.Nil(t, pl.closestQueued) - - pid2 := kadtest.NewStringID("1DoooUnknownPeer2") - - // add unknown peer 2, same as before, but there is no successor to pid2 - pl.addToPeerlist([]kad.NodeID[key.Key256]{pid2}) - require.Equal(t, nPeers-popCounter+1, pl.queuedCount) - require.Equal(t, pid2, pl.closestQueued.id) - // 5 peers have been poped so far, pid is not counted because unreachable - require.Nil(t, pl.popClosestQueued()) - // pid is set as unreachable, even though it was not popped - require.Equal(t, unreachable, pl.closest.next.next.next.next.next.next.status) -} diff --git a/query/simplequery/query.go b/query/simplequery/query.go deleted file mode 100644 index 7d6dda7..0000000 --- a/query/simplequery/query.go +++ /dev/null @@ -1,297 +0,0 @@ -package simplequery - -import ( - "context" - "errors" - "strconv" - "time" - - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" - "github.com/plprobelab/go-kademlia/util" -) - -// note that the returned []kad.NodeID are expected to be of the same type -// as the type returned by the routing table's NearestNodes method. the -// kad.NodeID returned by resp.CloserNodes() is not necessarily of the same -// type as the one returned by the routing table's NearestNodes method. so -// kad.NodeID s may need to be converted in this function. -type HandleResultFn[K kad.Key[K], A kad.Address[A]] func(context.Context, kad.NodeID[K], - kad.Response[K, A]) (bool, []kad.NodeID[K]) - -type NotifyFailureFn func(context.Context) - -type SimpleQuery[K kad.Key[K], A kad.Address[A]] struct { - ctx context.Context - self kad.NodeID[K] - done bool - protoID address.ProtocolID - req kad.Request[K, A] - concurrency int - peerstoreTTL time.Duration - timeout time.Duration - - msgEndpoint endpoint.Endpoint[K, A] - rt kad.RoutingTable[K, kad.NodeID[K]] - sched event.Scheduler - - inflightRequests int // requests that are either in flight or scheduled - peerlist *PeerList[K, A] - - // response handling - handleResultFn HandleResultFn[K, A] - // failure callback - notifyFailureFn NotifyFailureFn -} - -// NewSimpleQuery creates a new SimpleQuery. It initializes the query by adding -// the closest peers to the target key from the provided routing table to the -// query's peerlist. It sends `concurreny` requests events to the provided event -// queue. The requests events and followup events are handled by the event queue -// reader, and the parameters to these events are determined by the query's -// parameters. The query keeps track of the closest known peers to the target -// key, and the peers that have been queried so far. -func NewSimpleQuery[K kad.Key[K], A kad.Address[A]](ctx context.Context, self kad.NodeID[K], req kad.Request[K, A], - opts ...Option[K, A], -) (*SimpleQuery[K, A], error) { - ctx, span := util.StartSpan(ctx, "SimpleQuery.NewSimpleQuery", - trace.WithAttributes(attribute.String("Target", key.HexString(req.Target())))) - defer span.End() - - // apply options - var cfg Config[K, A] - if err := cfg.Apply(append([]Option[K, A]{DefaultConfig[K, A]}, opts...)...); err != nil { - span.RecordError(err) - return nil, err - } - - // get the closest peers to the target from the routing table - closestPeers := cfg.RoutingTable.NearestNodes(req.Target(), cfg.NumberUsefulCloserPeers) - if len(closestPeers) == 0 { - err := errors.New("no peers in routing table") - span.RecordError(err) - return nil, err - } - - // create new empty peerlist - pl := newPeerList(req.Target(), cfg.Endpoint) - // add the closest peers to peerlist - pl.addToPeerlist(closestPeers) - - q := &SimpleQuery[K, A]{ - ctx: ctx, - req: req, - self: self, - protoID: cfg.ProtocolID, - concurrency: cfg.Concurrency, - timeout: cfg.RequestTimeout, - peerstoreTTL: cfg.PeerstoreTTL, - rt: cfg.RoutingTable, - msgEndpoint: cfg.Endpoint, - sched: cfg.Scheduler, - handleResultFn: cfg.HandleResultsFunc, - notifyFailureFn: cfg.NotifyFailureFunc, - peerlist: pl, - } - - // add concurrency number of requests to eventqueue - q.enqueueNewRequests(ctx) - - return q, nil -} - -// checkIfDone cheks if the query is done, and return an error if it is done. -func (q *SimpleQuery[K, A]) checkIfDone() error { - if q.done { - // query is done, don't send any more requests - return errors.New("query done") - } - if q.ctx.Err() != nil { - q.done = true - return q.ctx.Err() - } - return nil -} - -// enqueueNewRequests adds the maximal number of requests to the scheduler's -// event queue. The maximal number of conccurent requests is limited by the -// concurrency factor and by the number of queued peers in the peerlist. -func (q *SimpleQuery[K, A]) enqueueNewRequests(ctx context.Context) { - ctx, span := util.StartSpan(ctx, "SimpleQuery.enqueueNewRequests") - defer span.End() - - // we always want to have the maximal number of requests in flight - newRequestsToSend := q.concurrency - q.inflightRequests - if q.peerlist.queuedCount < newRequestsToSend { - newRequestsToSend = q.peerlist.queuedCount - } - - if newRequestsToSend == 0 && q.inflightRequests == 0 { - // no more requests to send and no requests in flight, query has failed - // and is done - q.done = true - span.AddEvent("all peers queried") - q.notifyFailureFn(ctx) - return - } - - span.AddEvent("newRequestsToSend: " + strconv.Itoa(newRequestsToSend) + - " q.inflightRequests: " + strconv.Itoa(q.inflightRequests)) - - for i := 0; i < newRequestsToSend; i++ { - // add new pending request(s) for this query to eventqueue - q.sched.EnqueueAction(ctx, event.BasicAction(q.newRequest)) - } - // increase number of inflight requests. Note that it counts both queued - // requests and requests in flight - q.inflightRequests += newRequestsToSend - span.AddEvent("Enqueued " + strconv.Itoa(newRequestsToSend) + - " SimpleQuery.newRequest") -} - -// newRequest sends a request to the closest peer that hasn't been queried yet. -func (q *SimpleQuery[K, A]) newRequest(ctx context.Context) { - ctx, span := util.StartSpan(ctx, "SimpleQuery.newRequest") - defer span.End() - - if err := q.checkIfDone(); err != nil { - span.RecordError(err) - // decrease counter, because there is one less request queued - q.inflightRequests-- - return - } - - // get the closest peer from target that hasn't been queried yet - id := q.peerlist.popClosestQueued() - if id == nil { - // the peer list is empty, we don't have any more peers to query. This - // shouldn't happen because enqueueNewRequests doesn't enqueue more - // requests than there are queued peers in the peerlist - q.inflightRequests-- - return - } - span.AddEvent("Peer selected: " + id.String()) - - // this function will be queued when a response is received, an error - // occures or the request times out (with appropriate parameters) - handleResp := func(ctx context.Context, resp kad.Response[K, A], - err error, - ) { - if err != nil { - q.requestError(ctx, id, err) - } else { - q.handleResponse(ctx, id, resp) - } - } - // send request - err := q.msgEndpoint.SendRequestHandleResponse(ctx, q.protoID, id, q.req, - q.req.EmptyResponse(), q.timeout, handleResp) - if err != nil { - // there was an error before the request was sent, handle it - span.RecordError(err) - q.requestError(ctx, id, err) - } -} - -// handleResponse handles a response to a past query request -func (q *SimpleQuery[K, A]) handleResponse(ctx context.Context, id kad.NodeID[K], - resp kad.Response[K, A], -) { - ctx, span := util.StartSpan(ctx, "SimpleQuery.handleResponse", - trace.WithAttributes(attribute.String("Target", key.HexString(q.req.Target())), - attribute.String("From Peer", id.String()))) - defer span.End() - - if err := q.checkIfDone(); err != nil { - // request completed or was cancelled was the message was in flight, - // don't handle the message - span.RecordError(err) - return - } - - if resp == nil { - err := errors.New("nil response") - span.RecordError(err) - q.requestError(ctx, id, err) - return - } - - closerPeers := resp.CloserNodes() - if len(closerPeers) > 0 { - // consider that remote peer is behaving correctly if it returns - // at least 1 peer. We add it to our routing table only if it behaves - // as expected (we don't want to add unresponsive nodes to the rt) - q.rt.AddNode(id) - } - - q.inflightRequests-- - - // set peer as queried in the peerlist - q.peerlist.queriedPeer(id) - - // handle the response using the function provided by the caller, this - // function decides whether the query should terminate and returns the list - // of useful nodes that should be queried next - stop, usefulNodeIDs := q.handleResultFn(ctx, id, resp) - if stop { - // query is done, don't send any more requests - span.AddEvent("query over") - q.done = true - return - } - - // remove all occurreneces of q.self from usefulNodeIDs - writeIndex := 0 - for _, id := range usefulNodeIDs { - if !key.Equal(q.self.Key(), id.Key()) { - // id is valid and isn't self - usefulNodeIDs[writeIndex] = id - writeIndex++ - } else { - span.AddEvent("never add self to query peerlist") - } - } - usefulNodeIDs = usefulNodeIDs[:writeIndex] - - // add usefulNodeIDs to peerlist - q.peerlist.addToPeerlist(usefulNodeIDs) - - // enqueue new query requests to the event loop (usually 1) - q.enqueueNewRequests(ctx) -} - -// requestError handle an error that occured while sending a request or -// receiving a response. -func (q *SimpleQuery[K, A]) requestError(ctx context.Context, id kad.NodeID[K], err error) { - ctx, span := util.StartSpan(ctx, "SimpleQuery.requestError", - trace.WithAttributes(attribute.String("PeerID", id.String()), - attribute.String("Error", err.Error()))) - defer span.End() - - // the request isn't in flight anymore since it failed - q.inflightRequests-- - - if q.ctx.Err() == nil { - // remove peer from routing table unless context was cancelled. We don't - // want to keep peers that timed out or peers that returned nil/invalid - // responses. - q.rt.RemoveKey(id.Key()) - } - - if err := q.checkIfDone(); err != nil { - span.RecordError(err) - return - } - - // set peer as unreachable in the peerlist - q.peerlist.unreachablePeer(id) - - // enqueue new query requests to the event loop (usually 1) - q.enqueueNewRequests(ctx) -} diff --git a/query/simplequery/query_test.go b/query/simplequery/query_test.go deleted file mode 100644 index e5c52f0..0000000 --- a/query/simplequery/query_test.go +++ /dev/null @@ -1,714 +0,0 @@ -package simplequery - -import ( - "context" - "errors" - "fmt" - "net" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/routing/simplert" - "github.com/plprobelab/go-kademlia/server" - "github.com/plprobelab/go-kademlia/sim" -) - -// has dependency on basicserver which has dependecny on libp2p -> commented out -// -//// TestTrivialQuery tests a simple query from node0 to node1. node1 responds -//// with a single peer (node2) that will be in turn queried too, but node2 won't -//// return any closer peers -//func TestTrivialQuery(t *testing.T) { -// ctx := context.Background() -// clk := clock.NewMock() -// -// protoID := address.ProtocolID("/test/1.0.0") -// peerstoreTTL := 10 * time.Minute -// -// router := sim.NewRouter[key.Key256, net.IP]() -// node0 := kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(key.ZeroKey256()), nil) -// node1 := kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0x80})), nil) -// sched0 := ss.NewSimpleScheduler(clk) -// sched1 := ss.NewSimpleScheduler(clk) -// fendpoint0 := sim.NewEndpoint[key.Key256, net.IP](node0.ID(), sched0, router) -// fendpoint1 := sim.NewEndpoint[key.Key256, net.IP](node1.ID(), sched1, router) -// rt0 := simplert.New[key.Key256, net.IP](node0.ID().Key(), 1) -// rt1 := simplert.New[key.Key256, net.IP](node1.ID().Key(), 1) -// -// // make node1 a server -// server1 := basicserver.NewBasicServer[net.IP](rt1, fendpoint1) -// fendpoint1.AddRequestHandler(protoID, &sim.Message[key.Key256, net.IP]{}, server1.HandleRequest) -// -// // connect add node1 address to node0 -// err := fendpoint0.MaybeAddToPeerstore(ctx, node1, peerstoreTTL) -// require.NoError(t, err) -// success := rt0.AddNode(node1.ID()) -// require.True(t, success) -// -// // add a peer in node1's routing table. this peer will be returned in the -// // response to the query -// node2 := kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0xf0})), nil) -// err = fendpoint1.MaybeAddToPeerstore(ctx, node2, peerstoreTTL) -// require.NoError(t, err) -// success = rt1.AddNode(node2.ID()) -// require.True(t, success) -// -// req := sim.NewRequest[key.Key256, net.IP](kadtest.Key256WithLeadingBytes([]byte{0xf0})) -// -// queryOpts := []Option[key.Key256, net.IP]{ -// WithProtocolID[key.Key256, net.IP](protoID), -// WithConcurrency[key.Key256, net.IP](1), -// WithNumberUsefulCloserPeers[key.Key256, net.IP](2), -// WithRequestTimeout[key.Key256, net.IP](time.Second), -// WithPeerstoreTTL[key.Key256, net.IP](peerstoreTTL), -// WithRoutingTable[key.Key256, net.IP](rt0), -// WithEndpoint[key.Key256, net.IP](fendpoint0), -// WithScheduler[key.Key256, net.IP](sched0), -// } -// q, err := NewSimpleQuery[key.Key256, net.IP](ctx, node1.ID(), req, queryOpts...) -// require.NoError(t, err) -// -// // create and run the simulation -// sim := sim.NewLiteSimulator(clk) -// sim.AddPeers(sim, sched0, sched1) -// sim.Run(ctx) -// -// // check that the peerlist should contain node2 and node1 (in this order) -// require.Equal(t, node2.ID(), q.peerlist.closest.id) -// require.Equal(t, unreachable, q.peerlist.closest.status) -// // node2 is considered unreachable, and not added to the routing table, -// // because it doesn't return any peer (as its routing table is empty) -// require.Equal(t, node1.ID(), q.peerlist.closest.next.id) -// // node1 is set as queried because it answer with closer peers (only 1) -// require.Equal(t, queried, q.peerlist.closest.next.status) -// // there are no more peers in the peerlist -// require.Nil(t, q.peerlist.closest.next.next) -// require.Nil(t, q.peerlist.closestQueued) -//} - -func TestInvalidQueryOptions(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - router := sim.NewRouter[key.Key256, net.IP]() - node := kadtest.StringID("node0") - sched := event.NewSimpleScheduler(clk) - fendpoint := sim.NewEndpoint[key.Key256, net.IP](node, sched, router) - rt := simplert.New[key.Key256, kad.NodeID[key.Key256]](node, 1) - - // fails because rt is not set - invalidOpts := []Option[key.Key256, net.IP]{} - req := sim.NewRequest[key.Key256, net.IP](key.ZeroKey256()) - _, err := NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) - - // fails because rt is nil - invalidOpts = []Option[key.Key256, net.IP]{ - WithRoutingTable[key.Key256, net.IP](nil), - } - _, err = NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) - - // fails because endpoint is not set - invalidOpts = []Option[key.Key256, net.IP]{ - WithRoutingTable[key.Key256, net.IP](rt), - } - _, err = NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) - - // fails because endpoint is nil - invalidOpts = []Option[key.Key256, net.IP]{ - WithRoutingTable[key.Key256, net.IP](rt), - WithEndpoint[key.Key256, net.IP](nil), - } - _, err = NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) - - // fails because scheduler is not set - invalidOpts = []Option[key.Key256, net.IP]{ - WithRoutingTable[key.Key256, net.IP](rt), - WithEndpoint[key.Key256, net.IP](fendpoint), - } - _, err = NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) - - // fails because scheduler is nil - invalidOpts = []Option[key.Key256, net.IP]{ - WithRoutingTable[key.Key256, net.IP](rt), - WithEndpoint[key.Key256, net.IP](fendpoint), - WithScheduler[key.Key256, net.IP](nil), - } - _, err = NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) - - // fails because HandleResultsFunc is nil - invalidOpts = []Option[key.Key256, net.IP]{ - WithRoutingTable[key.Key256, net.IP](rt), - WithEndpoint[key.Key256, net.IP](fendpoint), - WithScheduler[key.Key256, net.IP](sched), - WithHandleResultsFunc[key.Key256, net.IP](nil), - } - _, err = NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) - - // fails because WithNotifyFailureFunc is nil - invalidOpts = []Option[key.Key256, net.IP]{ - WithRoutingTable[key.Key256, net.IP](rt), - WithEndpoint[key.Key256, net.IP](fendpoint), - WithScheduler[key.Key256, net.IP](sched), - WithNotifyFailureFunc[key.Key256, net.IP](nil), - } - _, err = NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) - - // fails because NumberUsefulCloserPeers is 0 - invalidOpts = []Option[key.Key256, net.IP]{ - WithRoutingTable[key.Key256, net.IP](rt), - WithEndpoint[key.Key256, net.IP](fendpoint), - WithScheduler[key.Key256, net.IP](sched), - WithNumberUsefulCloserPeers[key.Key256, net.IP](0), - } - _, err = NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) - - // fails because Concurrency is 0 - invalidOpts = []Option[key.Key256, net.IP]{ - WithRoutingTable[key.Key256, net.IP](rt), - WithEndpoint[key.Key256, net.IP](fendpoint), - WithScheduler[key.Key256, net.IP](sched), - WithConcurrency[key.Key256, net.IP](0), - } - _, err = NewSimpleQuery[key.Key256, net.IP](ctx, node, req, invalidOpts...) - require.Error(t, err) -} - -func simulationSetup(t *testing.T, ctx context.Context, n, bucketSize int, - clk clock.Clock, protoID address.ProtocolID, peerstoreTTL time.Duration, - defaultQueryOpts []Option[key.Key8, net.IP]) ( - []kad.NodeInfo[key.Key8, net.IP], []event.AwareScheduler, []sim.SimEndpoint[key.Key8, net.IP], - []kad.RoutingTable[key.Key8, kad.NodeID[key.Key8]], []server.Server[key.Key8], [][]Option[key.Key8, net.IP], -) { - router := sim.NewRouter[key.Key8, net.IP]() - - ids := make([]kad.NodeInfo[key.Key8, net.IP], n) - scheds := make([]event.AwareScheduler, n) - fendpoints := make([]sim.SimEndpoint[key.Key8, net.IP], n) - rts := make([]kad.RoutingTable[key.Key8, kad.NodeID[key.Key8]], n) - servers := make([]server.Server[key.Key8], n) - - spacing := 256 / n - - for i := 0; i < n; i++ { - scheds[i] = event.NewSimpleScheduler(clk) - ids[i] = kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(uint8(i*spacing))), nil) - fendpoints[i] = sim.NewEndpoint[key.Key8, net.IP](ids[i].ID(), scheds[i], router) - rts[i] = simplert.New[key.Key8, kad.NodeID[key.Key8]](ids[i].ID(), bucketSize) - cfg := sim.DefaultServerConfig() - cfg.NumberUsefulCloserPeers = bucketSize - servers[i] = sim.NewServer[key.Key8, net.IP](rts[i], fendpoints[i], cfg) - fendpoints[i].AddRequestHandler(protoID, &sim.Message[key.Key8, net.IP]{}, servers[i].HandleRequest) - } - - // peer ids (KadIDs) are i*8 for i in [0, 32), the keyspace is 1 byte [0, 255] - // so peers cover the whole keyspace, and they have the format XXXX X000. - - // connect peers, and add them to the routing tables - // note: when the bucket is complete, it contains the peers with the - // smallest identifier (KadID). - for i := 0; i < n; i++ { - for j := i + 1; j < n; j++ { - // add peer to peerstore - err := fendpoints[i].MaybeAddToPeerstore(ctx, ids[j], peerstoreTTL) - require.NoError(t, err) - // we don't require the the peer is added to the routing table, - // because the bucket might be full already and it is fine - rts[i].AddNode(ids[j].ID()) - } - } - - // query options for each peer - queryOpts := make([][]Option[key.Key8, net.IP], n) - for i := 0; i < n; i++ { - queryOpts[i] = append(defaultQueryOpts, - WithRoutingTable[key.Key8, net.IP](rts[i]), - WithEndpoint[key.Key8, net.IP](fendpoints[i]), - WithScheduler[key.Key8, net.IP](scheds[i]), - ) - } - - return ids, scheds, fendpoints, rts, servers, queryOpts -} - -func getHandleResults[K kad.Key[K], A kad.Address[A]](t *testing.T, req kad.Request[K, A], - expectedPeers []K, expectedResponses [][]K) func( - ctx context.Context, id kad.NodeID[K], resp kad.Response[K, A]) ( - bool, []kad.NodeID[K]) { - var responseCount int - return func(ctx context.Context, id kad.NodeID[K], - resp kad.Response[K, A], - ) (bool, []kad.NodeID[K]) { - // check that the request was sent to the correct peer - require.Equal(t, expectedPeers[responseCount], id.Key(), "responseCount: ", responseCount) - - ids := make([]kad.NodeID[K], len(resp.CloserNodes())) - var found bool - for i, n := range resp.CloserNodes() { - ids[i] = n.ID() - if key.Equal(ids[i].Key(), req.Target()) { - // the target was found, stop the query - found = true - } - // check that the response contains the expected peers - require.Contains(t, expectedResponses[responseCount], n.ID().Key()) - } - responseCount++ - return found, ids - } -} - -func TestElementaryQuery(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - protoID := address.ProtocolID("/test/1.0.0") - bucketSize := 4 - nPeers := 32 - peerstoreTTL := time.Minute - - // generic query options to be used by all peers - defaultQueryOpts := []Option[key.Key8, net.IP]{ - WithProtocolID[key.Key8, net.IP](protoID), - WithConcurrency[key.Key8, net.IP](1), - WithNumberUsefulCloserPeers[key.Key8, net.IP](bucketSize), - WithRequestTimeout[key.Key8, net.IP](time.Second), - WithPeerstoreTTL[key.Key8, net.IP](peerstoreTTL), - } - - ids, scheds, _, rts, _, queryOpts := simulationSetup(t, ctx, nPeers, - bucketSize, clk, protoID, peerstoreTTL, defaultQueryOpts) - - // smallest peer is looking for biggest peer (which is the most far away - // in hop numbers, given the routing table configuration) - req := sim.NewRequest[key.Key8, net.IP](ids[len(ids)-1].ID().Key()) - - // peers that are expected to be queried, in order - expectedPeers := []key.Key8{} - // peer that are expected to be included in responses, in order - expectedResponses := [][]key.Key8{} - - currID := 0 - // while currID != target.Key() - for !key.Equal(ids[currID].ID().Key(), req.Target()) { - // get closest peer to target from the sollicited peer - closest := rts[currID].NearestNodes(req.Target(), 1) - require.Len(t, closest, 1, fmt.Sprint(key.Equal(ids[currID].ID().Key(), req.Target()))) - expectedPeers = append(expectedPeers, closest[0].Key()) - - // the next current id is the closest peer to the target - currID = int(closest[0].Key() / 8) - - // the peers included in the response are the closest to the target - // from the sollicited peer - responseClosest := rts[currID].NearestNodes(req.Target(), bucketSize) - closestKey := make([]key.Key8, len(responseClosest)) - for i, n := range responseClosest { - closestKey[i] = n.Key() - } - expectedResponses = append(expectedResponses, closestKey) - } - - // handleResults is called when a peer receives a response from a peer. If - // the response contains the target, it returns true, and the query stops. - // Otherwise, it returns false, and the query continues. This function also - // checks that the response come from the expected peer and contains the - // expected peers addresses. - handleResults := getHandleResults[key.Key8, net.IP](t, req, expectedPeers, expectedResponses) - - // the request will not fail - notifyFailure := func(context.Context) { - require.Fail(t, "notify failure shouldn't be called") - } - - _, err := NewSimpleQuery[key.Key8, net.IP](ctx, ids[0].ID(), req, append(queryOpts[0], - WithHandleResultsFunc(handleResults), - WithNotifyFailureFunc[key.Key8, net.IP](notifyFailure))...) - require.NoError(t, err) - - // create simulator - s := sim.NewLiteSimulator(clk) - sim.AddSchedulers(s, scheds...) - // run simulation - s.Run(ctx) -} - -func TestFailedQuery(t *testing.T) { - // the key doesn't exist and cannot be found, test with no exit condition - // all peers of the peerlist should have been queried by the end - - ctx := context.Background() - clk := clock.NewMock() - - protoID := address.ProtocolID("/test/1.0.0") - bucketSize := 4 - nPeers := 16 - peerstoreTTL := time.Minute - - // generic query options to be used by all peers - defaultQueryOpts := []Option[key.Key8, net.IP]{ - WithProtocolID[key.Key8, net.IP](protoID), - WithConcurrency[key.Key8, net.IP](1), - WithNumberUsefulCloserPeers[key.Key8, net.IP](bucketSize), - WithRequestTimeout[key.Key8, net.IP](time.Second), - WithPeerstoreTTL[key.Key8, net.IP](peerstoreTTL), - } - - ids, scheds, _, rts, _, queryOpts := simulationSetup(t, ctx, nPeers, - bucketSize, clk, protoID, peerstoreTTL, defaultQueryOpts) - - // smallest peer is looking for biggest peer (which is the most far away - // in hop numbers, given the routing table configuration) - req := sim.NewRequest[key.Key8, net.IP](key.Key8(0xff)) - - // _______^_______ - // __^__ __^__ - // / \ / \ - // / \ / \ / \ / \ - // / \ / \ / \ / \ / \ / \ / \ / \ - // 0 1 2 3 4 5 6 7 8 9 a b c d e f - // * * * * // closest peers to 0xff in 0's routing - // // table, closest is b - // - // * * * * // closest peers to 0xff in b's routing - // // table, closest is f - // - // - 0 will add b, a, 9, 8 to the query's peerlist. - // - 0 queries b first, that respond with closer peers to 0xff: f, e, d, c - // - then 0 won't learn any more new peers, and query all the peerlist from - // the closest to the furthest from 0xff: f, e, d, c, a, 9, 8. - // - note that the closest peers to 0xff for 8, 9, a, b, c, d, e, f are - // always [f, e, d, c]. - - order := []int{0xb0, 0xf0, 0xe0, 0xd0, 0xc0, 0xa0, 0x90, 0x80} - for i, o := range order { - // 16 is the spacing between the nodes - order[i] = o / 16 - } - - // peers that are expected to be queried, in order - expectedPeers := make([]key.Key8, len(order)) - for i, o := range order { - expectedPeers[i] = ids[o].ID().Key() - } - - // peer that are expected to be included in responses, in order - expectedResponses := make([][]key.Key8, len(order)) - for i, o := range order { - // the peers included in the response are the closest to the target - // from the sollicited peer - responseClosest := rts[o].NearestNodes(req.Target(), bucketSize) - closestKeys := make([]key.Key8, len(responseClosest)) - for i, n := range responseClosest { - closestKeys[i] = n.Key() - } - expectedResponses[i] = closestKeys - } - - // handleResults is called when a peer receives a response from a peer. If - // the response contains the target, it returns true, and the query stops. - // Otherwise, it returns false, and the query continues. This function also - // checks that the response come from the expected peer and contains the - // expected peers addresses. - handleResults := getHandleResults[key.Key8, net.IP](t, req, expectedPeers, expectedResponses) - - var failed bool - // the request will not fail - notifyFailure := func(context.Context) { - failed = true - } - - _, err := NewSimpleQuery[key.Key8, net.IP](ctx, ids[0].ID(), req, append(queryOpts[0], - WithHandleResultsFunc(handleResults), - WithNotifyFailureFunc[key.Key8, net.IP](notifyFailure))...) - require.NoError(t, err) - - // create simulator - s := sim.NewLiteSimulator(clk) - sim.AddSchedulers(s, scheds...) - // run simulation - s.Run(ctx) - - require.True(t, failed) -} - -// has dependency on basicserver which has dependency on libp2p -> commented out -// -//func TestConcurrentQuery(t *testing.T) { -// ctx := context.Background() -// clk := clock.NewMock() -// -// protoID := address.ProtocolID("/test/1.0.0") -// bucketSize := 4 -// nPeers := 8 -// peerstoreTTL := time.Minute -// -// router := sim.NewRouter[key.Key256, net.IP]() -// -// ids := make([]kad.NodeInfo[key.Key256, net.IP], nPeers) -// scheds := make([]scheduler.AwareScheduler, nPeers) -// fendpoints := make([]endpoint.SimEndpoint[key.Key256, net.IP], nPeers) -// rts := make([]kad.RoutingTable[key.Key256], nPeers) -// servers := make([]server.Server[key.Key256], nPeers) -// for i := 0; i < nPeers; i++ { -// scheds[i] = ss.NewSimpleScheduler(clk) -// ids[i] = kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{byte(i * 32)})), nil) -// fendpoints[i] = sim.NewEndpoint(ids[i].ID(), scheds[i], router) -// rts[i] = simplert.New[key.Key256, net.IP](ids[i].ID().Key(), bucketSize) -// servers[i] = basicserver.NewBasicServer[net.IP](rts[i], fendpoints[i], basicserver.WithNumberUsefulCloserPeers(bucketSize)) -// fendpoints[i].AddRequestHandler(protoID, &sim.Message[key.Key256, net.IP]{}, servers[i].HandleRequest) -// } -// -// // 0 is looking for 7 -// // 0 knows 1, 2, 3: it will query 3, 2 at first -// // 3 knows 4, 0 -// // 2 knows 6, 0 -// // 4 knows 5, 3 -// // 6 knows 7, 2, 5 -// // 5 knows 4, 6 -// // sequence of outgoing requests from 0 to find 7: 3, 2, 4, 6 -// -// connections := [...][2]int{ -// {0, 1}, -// {0, 2}, -// {0, 3}, -// {3, 4}, -// {2, 6}, -// {4, 5}, -// {6, 7}, -// {6, 5}, -// } -// -// for _, c := range connections { -// for i := range c { -// // add peer to peerstore -// err := fendpoints[c[i]].MaybeAddToPeerstore(ctx, ids[c[1-i]], peerstoreTTL) -// require.NoError(t, err) -// // we don't require the the peer is added to the routing table, -// // because the bucket might be full already and it is fine -// rts[c[i]].AddNode(ids[c[1-i]].ID()) -// } -// } -// -// // generic query options to be used by all peers -// defaultQueryOpts := []Option[key.Key256, net.IP]{ -// WithProtocolID[key.Key256, net.IP](protoID), -// WithConcurrency[key.Key256, net.IP](2), -// WithNumberUsefulCloserPeers[key.Key256, net.IP](bucketSize), -// WithRequestTimeout[key.Key256, net.IP](time.Second), -// WithPeerstoreTTL[key.Key256, net.IP](peerstoreTTL), -// } -// -// // query options for each peer -// queryOpts := make([][]Option[key.Key256, net.IP], nPeers) -// for i := 0; i < nPeers; i++ { -// queryOpts[i] = append(defaultQueryOpts, -// WithRoutingTable[key.Key256, net.IP](rts[i]), -// WithEndpoint[key.Key256, net.IP](fendpoints[i]), -// WithScheduler[key.Key256, net.IP](scheds[i]), -// ) -// } -// -// // smallest peer is looking for biggest peer (which is the most far away -// // in hop numbers, given the routing table configuration) -// req := sim.NewRequest[key.Key256, net.IP](ids[len(ids)-1].ID().Key()) -// -// // peers that are expected to be queried, in order -// expectedPeers := []key.Key256{ -// ids[3].ID().Key(), ids[2].ID().Key(), -// ids[4].ID().Key(), ids[6].ID().Key(), -// } -// // peer that are expected to be included in responses, in order -// expectedResponses := [][]key.Key256{ -// {ids[4].ID().Key(), ids[0].ID().Key()}, -// {ids[6].ID().Key(), ids[0].ID().Key()}, -// {ids[5].ID().Key(), ids[3].ID().Key()}, -// {ids[7].ID().Key(), ids[2].ID().Key(), ids[5].ID().Key()}, -// } -// -// handleResults := getHandleResults[key.Key256, net.IP](t, req, expectedPeers, expectedResponses) -// -// // the request will not fail -// notifyFailure := func(context.Context) { -// require.Fail(t, "notify failure shouldn't be called") -// } -// -// _, err := NewSimpleQuery[key.Key256, net.IP](ctx, nil, req, append(queryOpts[0], -// WithHandleResultsFunc(handleResults), -// WithNotifyFailureFunc[key.Key256, net.IP](notifyFailure))...) -// require.NoError(t, err) -// -// // create simulator -// sim := sim.NewLiteSimulator(clk) -// sim.AddPeers(sim, scheds...) -// // run simulation -// sim.Run(ctx) -//} - -func TestUnresponsivePeer(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - protoID := address.ProtocolID("/test/1.0.0") - peerstoreTTL := time.Minute - bucketSize := 1 - - router := sim.NewRouter[key.Key8, net.IP]() - node0 := kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0)), nil) - node1 := kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(1)), nil) - sched0 := event.NewSimpleScheduler(clk) - sched1 := event.NewSimpleScheduler(clk) - fendpoint0 := sim.NewEndpoint[key.Key8, net.IP](node0.ID(), sched0, router) - fendpoint1 := sim.NewEndpoint[key.Key8, net.IP](node1.ID(), sched1, router) - rt0 := simplert.New[key.Key8, kad.NodeID[key.Key8]](node0.ID(), bucketSize) - - serverRequestHandler := func(context.Context, kad.NodeID[key.Key8], - kad.Message, - ) (kad.Message, error) { - return nil, errors.New("") - } - fendpoint1.AddRequestHandler(protoID, &sim.Message[key.Key8, net.IP]{}, serverRequestHandler) - - req := sim.NewRequest[key.Key8, net.IP](key.Key8(0xff)) - - responseHandler := func(ctx context.Context, sender kad.NodeID[key.Key8], - msg kad.Response[key.Key8, net.IP], - ) (bool, []kad.NodeID[key.Key8]) { - require.Fail(t, "response handler shouldn't be called") - return false, nil - } - queryOpts := []Option[key.Key8, net.IP]{ - WithProtocolID[key.Key8, net.IP](protoID), - WithConcurrency[key.Key8, net.IP](2), - WithNumberUsefulCloserPeers[key.Key8, net.IP](bucketSize), - WithRequestTimeout[key.Key8, net.IP](time.Millisecond), - WithEndpoint[key.Key8, net.IP](fendpoint0), - WithRoutingTable[key.Key8, net.IP](rt0), - WithScheduler[key.Key8, net.IP](sched0), - WithHandleResultsFunc[key.Key8, net.IP](responseHandler), - } - - // query creation fails because the routing table of 0 is empty - _, err := NewSimpleQuery[key.Key8, net.IP](ctx, nil, req, queryOpts...) - require.Error(t, err) - - // connect 0 and 1 - err = fendpoint0.MaybeAddToPeerstore(ctx, node1, peerstoreTTL) - require.NoError(t, err) - success := rt0.AddNode(node1.ID()) - require.True(t, success) - - q, err := NewSimpleQuery[key.Key8, net.IP](ctx, nil, req, queryOpts...) - require.NoError(t, err) - - // create simulator - s := sim.NewLiteSimulator(clk) - sim.AddSchedulers(s, sched0, sched1) - // run simulation - s.Run(ctx) - - // make sure the peer is marked as unreachable - require.Equal(t, unreachable, q.peerlist.closest.status) -} - -func TestCornerCases(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - clk := clock.NewMock() - - protoID := address.ProtocolID("/test/1.0.0") - bucketSize := 1 - peerstoreTTL := time.Minute - - router := sim.NewRouter[key.Key8, net.IP]() - node0 := kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0x00)), nil) - sched0 := event.NewSimpleScheduler(clk) - fendpoint0 := sim.NewEndpoint(node0.ID(), sched0, router) - rt0 := simplert.New[key.Key8, kad.NodeID[key.Key8]](node0.ID(), bucketSize) - - node1 := kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0x01)), nil) - fendpoint0.MaybeAddToPeerstore(ctx, node1, peerstoreTTL) - - success := rt0.AddNode(node1.ID()) - require.True(t, success) - - req := sim.NewRequest[key.Key8, net.IP](key.Key8(0xff)) - - responseHandler := func(ctx context.Context, sender kad.NodeID[key.Key8], - msg kad.Response[key.Key8, net.IP], - ) (bool, []kad.NodeID[key.Key8]) { - ids := make([]kad.NodeID[key.Key8], len(msg.CloserNodes())) - for i, peer := range msg.CloserNodes() { - ids[i] = peer.ID() - } - return false, ids - } - - queryOpts := []Option[key.Key8, net.IP]{ - WithProtocolID[key.Key8, net.IP](protoID), - WithConcurrency[key.Key8, net.IP](1), - WithNumberUsefulCloserPeers[key.Key8, net.IP](bucketSize), - WithRequestTimeout[key.Key8, net.IP](time.Millisecond), - WithEndpoint[key.Key8, net.IP](fendpoint0), - WithRoutingTable[key.Key8, net.IP](rt0), - WithScheduler[key.Key8, net.IP](sched0), - WithHandleResultsFunc(responseHandler), - } - - q, err := NewSimpleQuery[key.Key8, net.IP](ctx, node0.ID(), req, queryOpts...) - require.NoError(t, err) - - // nil response should trigger a request error - q.handleResponse(ctx, node1.ID(), nil) - - addrs := []kad.NodeInfo[key.Key8, net.IP]{ - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0xee)), nil), - // kadtest.NewInfo()(kadtest.NewID()([]byte{0x66, 0x66}), nil), // invalid key length - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0x88)), nil), - } - forgedResponse := sim.NewResponse(addrs) - q.handleResponse(ctx, node1.ID(), forgedResponse) - - // test that 0xee and 0x88 have been added to peerlist but not 0x6666 - require.Equal(t, addrs[0].ID(), q.peerlist.closest.id) - require.Equal(t, addrs[1].ID(), q.peerlist.closest.next.id) - require.Equal(t, node1.ID(), q.peerlist.closest.next.next.id) - require.Nil(t, q.peerlist.closest.next.next.next) - - // test new request if all peers have been tried - for q.peerlist.closestQueued != nil { - q.peerlist.popClosestQueued() - } - q.inflightRequests = 1 - q.newRequest(ctx) - require.Equal(t, 0, q.inflightRequests) - - // cancel contex - cancel() - sched0.EnqueueAction(ctx, event.BasicAction(func(context.Context) { - q.requestError(ctx, node1.ID(), errors.New("")) - })) - - // create simulator - s := sim.NewLiteSimulator(clk) - sim.AddSchedulers(s, sched0) - // run simulation - s.Run(ctx) - - require.True(t, q.done) -} diff --git a/routing.go b/routing.go new file mode 100644 index 0000000..020ea9c --- /dev/null +++ b/routing.go @@ -0,0 +1,38 @@ +package libdht + +// RoutingTable is the interface all DHT Routing Tables types support. +type RoutingTable[D Distance[D], P Point[P, D], N NodeID[D, P]] interface { + // AddNode tries to add a peer to the routing table. It returns true if + // the node was added and false if it wasn't added, e.g., because it + // was already part of the routing table. + // + // Because NodeID[K]'s are often preimages to Kademlia keys K + // there's no way to derive a NodeID[K] from just K. Therefore, to be + // able to return NodeID[K]'s from the `NearestNodes` method, this + // `AddNode` method signature takes a NodeID[K] instead of only K. + // + // Nodes added to the routing table are grouped into buckets based on their + // XOR distance to the local node's identifier. The details of the XOR + // arithmetics are defined on K. + AddNode(N) bool + + // RemoveKey tries to remove a node identified by its Kademlia key from the + // routing table. + // + // It returns true if the key existed in the routing table and was removed. + // It returns false if the key didn't exist in the routing table and + // therefore, was not removed. + RemoveNode(N) bool + + // NearestNodes returns the given number of closest nodes to a given + // Kademlia key that are currently present in the routing table. + // The returned list of nodes will be ordered from closest to furthest and + // contain at maximum the given number of entries, but also possibly less + // if the number exceeds the number of nodes in the routing table. + NearestNodes(P, int) []N + + // GetNode returns the node identified by the supplied Kademlia key or a zero + // value if the node is not present in the routing table. The boolean second + // return value indicates whether the node was found in the table. + GetNode(P) (N, bool) +} diff --git a/routing/README.md b/routing/README.md deleted file mode 100644 index 92ef379..0000000 --- a/routing/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Routing Table - -`Table` defines a generic Kademlia routing table interface. - -```go -// Table is the interface for Kademlia Routing Tables -type Table interface { - // Self returns the local node's Kademlia key - Self() key.KadKey - // AddPeer tries to add a peer to the routing table - AddPeer(context.Context, kad.NodeID) (bool, error) - // RemovePeer tries to remove a peer identified by its Kademlia key from the - // routing table - RemoveKey(context.Context, key.KadKey) (bool, error) - // NearestPeers returns the closest peers to a given key - NearestPeers(context.Context, key.KadKey, int) ([]kad.NodeID, error) -} -``` - -In `go-libp2p-kad-dht`, the Routing Table periodically refreshes. This operation consists in looking for its own Kademlia key, to be aware of one's closest neighbors at all time, and making sure that the buckets are _as full as possible_ with reachable peers. So a node will make sure that all peers that are in its routing table are still online, and will replace the offline peers with fresh ones. - -## Implementations - -- `SimpleRT` a very simple routing table implementation that should NOT be used in production. -- `ClientRT` (doesn't exist yet) a routing table implementation that is optimized for nodes in client mode only -- `TrieRT` (doesn't exist yet) a routing table implementation based on a binary trie to store Kademlia keys and optimize distance computations. -- `FullRT` (not migrated yet) a routing table implementation that periodically crawls the network and stores all nodes. -- `LazyRT` (doesn't exist yet) a routing table implementation keeping all peers it has heard of in its routing table, but only refreshes a subset of them periodically. Some peers may be unreachable. - -## Challenges - -2023-05-23: We want to keep track of the remote Clients that are close to us. So we want to add them in our routing table. However, we don't want to give them as _closer peers_ when answering a `FIND_NODE` request. They should remain in the RT (as long as there is space in the buckets), but not be shared. They should be kept in the routing table, but they aren't prioritary compared with other DHT servers. diff --git a/routing/bootstrap.go b/routing/bootstrap.go deleted file mode 100644 index 96e0a4d..0000000 --- a/routing/bootstrap.go +++ /dev/null @@ -1,253 +0,0 @@ -package routing - -import ( - "context" - "fmt" - "time" - - "github.com/benbjohnson/clock" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/kaderr" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/query" - "github.com/plprobelab/go-kademlia/util" -) - -type Bootstrap[K kad.Key[K], A kad.Address[A]] struct { - // self is the node id of the system the bootstrap is running on - self kad.NodeID[K] - - // qry is the query used by the bootstrap process - qry *query.Query[K, A] - - // cfg is a copy of the optional configuration supplied to the Bootstrap - cfg BootstrapConfig[K, A] -} - -// BootstrapConfig specifies optional configuration for a Bootstrap -type BootstrapConfig[K kad.Key[K], A kad.Address[A]] struct { - Timeout time.Duration // the time to wait before terminating a query that is not making progress - RequestConcurrency int // the maximum number of concurrent requests that each query may have in flight - RequestTimeout time.Duration // the timeout queries should use for contacting a single node - Clock clock.Clock // a clock that may replaced by a mock when testing -} - -// Validate checks the configuration options and returns an error if any have invalid values. -func (cfg *BootstrapConfig[K, A]) Validate() error { - if cfg.Clock == nil { - return &kaderr.ConfigurationError{ - Component: "BootstrapConfig", - Err: fmt.Errorf("clock must not be nil"), - } - } - - if cfg.Timeout < 1 { - return &kaderr.ConfigurationError{ - Component: "BootstrapConfig", - Err: fmt.Errorf("timeout must be greater than zero"), - } - } - - if cfg.RequestConcurrency < 1 { - return &kaderr.ConfigurationError{ - Component: "BootstrapConfig", - Err: fmt.Errorf("request concurrency must be greater than zero"), - } - } - - if cfg.RequestTimeout < 1 { - return &kaderr.ConfigurationError{ - Component: "BootstrapConfig", - Err: fmt.Errorf("request timeout must be greater than zero"), - } - } - - return nil -} - -// DefaultBootstrapConfig returns the default configuration options for a Bootstrap. -// Options may be overridden before passing to NewBootstrap -func DefaultBootstrapConfig[K kad.Key[K], A kad.Address[A]]() *BootstrapConfig[K, A] { - return &BootstrapConfig[K, A]{ - Clock: clock.New(), // use standard time - Timeout: 5 * time.Minute, - RequestConcurrency: 3, - RequestTimeout: time.Minute, - } -} - -func NewBootstrap[K kad.Key[K], A kad.Address[A]](self kad.NodeID[K], cfg *BootstrapConfig[K, A]) (*Bootstrap[K, A], error) { - if cfg == nil { - cfg = DefaultBootstrapConfig[K, A]() - } else if err := cfg.Validate(); err != nil { - return nil, err - } - - return &Bootstrap[K, A]{ - self: self, - cfg: *cfg, - }, nil -} - -// Advance advances the state of the bootstrap by attempting to advance its query if running. -func (b *Bootstrap[K, A]) Advance(ctx context.Context, ev BootstrapEvent) BootstrapState { - ctx, span := util.StartSpan(ctx, "Bootstrap.Advance") - defer span.End() - - switch tev := ev.(type) { - case *EventBootstrapStart[K, A]: - - // TODO: ignore start event if query is already in progress - iter := query.NewClosestNodesIter(b.self.Key()) - - qryCfg := query.DefaultQueryConfig[K]() - qryCfg.Clock = b.cfg.Clock - qryCfg.Concurrency = b.cfg.RequestConcurrency - qryCfg.RequestTimeout = b.cfg.RequestTimeout - - queryID := query.QueryID("bootstrap") - - qry, err := query.NewQuery[K](b.self, queryID, tev.ProtocolID, tev.Message, iter, tev.KnownClosestNodes, qryCfg) - if err != nil { - // TODO: don't panic - panic(err) - } - b.qry = qry - return b.advanceQuery(ctx, nil) - - case *EventBootstrapMessageResponse[K, A]: - return b.advanceQuery(ctx, &query.EventQueryMessageResponse[K, A]{ - NodeID: tev.NodeID, - Response: tev.Response, - }) - case *EventBootstrapMessageFailure[K]: - return b.advanceQuery(ctx, &query.EventQueryMessageFailure[K]{ - NodeID: tev.NodeID, - Error: tev.Error, - }) - - case *EventBootstrapPoll: - // ignore, nothing to do - default: - panic(fmt.Sprintf("unexpected event: %T", tev)) - } - - if b.qry != nil { - return b.advanceQuery(ctx, nil) - } - - return &StateBootstrapIdle{} -} - -func (b *Bootstrap[K, A]) advanceQuery(ctx context.Context, qev query.QueryEvent) BootstrapState { - state := b.qry.Advance(ctx, qev) - switch st := state.(type) { - case *query.StateQueryWaitingMessage[K, A]: - return &StateBootstrapMessage[K, A]{ - QueryID: st.QueryID, - Stats: st.Stats, - NodeID: st.NodeID, - ProtocolID: st.ProtocolID, - Message: st.Message, - } - case *query.StateQueryFinished: - return &StateBootstrapFinished{ - Stats: st.Stats, - } - case *query.StateQueryWaitingAtCapacity: - elapsed := b.cfg.Clock.Since(st.Stats.Start) - if elapsed > b.cfg.Timeout { - return &StateBootstrapTimeout{ - Stats: st.Stats, - } - } - return &StateBootstrapWaiting{ - Stats: st.Stats, - } - case *query.StateQueryWaitingWithCapacity: - elapsed := b.cfg.Clock.Since(st.Stats.Start) - if elapsed > b.cfg.Timeout { - return &StateBootstrapTimeout{ - Stats: st.Stats, - } - } - return &StateBootstrapWaiting{ - Stats: st.Stats, - } - default: - panic(fmt.Sprintf("unexpected state: %T", st)) - } -} - -// BootstrapState is the state of a bootstrap. -type BootstrapState interface { - bootstrapState() -} - -// StateBootstrapMessage indicates that the bootstrap query is waiting to message a node. -type StateBootstrapMessage[K kad.Key[K], A kad.Address[A]] struct { - QueryID query.QueryID - NodeID kad.NodeID[K] - ProtocolID address.ProtocolID - Message kad.Request[K, A] - Stats query.QueryStats -} - -// StateBootstrapIdle indicates that the bootstrap is not running its query. -type StateBootstrapIdle struct{} - -// StateBootstrapFinished indicates that the bootstrap has finished. -type StateBootstrapFinished struct { - Stats query.QueryStats -} - -// StateBootstrapTimeout indicates that the bootstrap query has timed out. -type StateBootstrapTimeout struct { - Stats query.QueryStats -} - -// StateBootstrapWaiting indicates that the bootstrap query is waiting for a response. -type StateBootstrapWaiting struct { - Stats query.QueryStats -} - -// bootstrapState() ensures that only Bootstrap states can be assigned to a BootstrapState. -func (*StateBootstrapMessage[K, A]) bootstrapState() {} -func (*StateBootstrapIdle) bootstrapState() {} -func (*StateBootstrapFinished) bootstrapState() {} -func (*StateBootstrapTimeout) bootstrapState() {} -func (*StateBootstrapWaiting) bootstrapState() {} - -// BootstrapEvent is an event intended to advance the state of a bootstrap. -type BootstrapEvent interface { - bootstrapEvent() -} - -// EventBootstrapPoll is an event that signals the bootstrap that it can perform housekeeping work such as time out queries. -type EventBootstrapPoll struct{} - -// EventBootstrapStart is an event that attempts to start a new bootstrap -type EventBootstrapStart[K kad.Key[K], A kad.Address[A]] struct { - ProtocolID address.ProtocolID - Message kad.Request[K, A] - KnownClosestNodes []kad.NodeID[K] -} - -// EventBootstrapMessageResponse notifies a bootstrap that a sent message has received a successful response. -type EventBootstrapMessageResponse[K kad.Key[K], A kad.Address[A]] struct { - NodeID kad.NodeID[K] // the node the message was sent to - Response kad.Response[K, A] // the message response sent by the node -} - -// EventBootstrapMessageFailure notifiesa bootstrap that an attempt to send a message has failed. -type EventBootstrapMessageFailure[K kad.Key[K]] struct { - NodeID kad.NodeID[K] // the node the message was sent to - Error error // the error that caused the failure, if any -} - -// bootstrapEvent() ensures that only Bootstrap events can be assigned to the BootstrapEvent interface. -func (*EventBootstrapPoll) bootstrapEvent() {} -func (*EventBootstrapStart[K, A]) bootstrapEvent() {} -func (*EventBootstrapMessageResponse[K, A]) bootstrapEvent() {} -func (*EventBootstrapMessageFailure[K]) bootstrapEvent() {} diff --git a/routing/bootstrap_test.go b/routing/bootstrap_test.go deleted file mode 100644 index 43dddf7..0000000 --- a/routing/bootstrap_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package routing - -import ( - "context" - "testing" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/query" -) - -func TestBootstrapConfigValidate(t *testing.T) { - t.Run("default is valid", func(t *testing.T) { - cfg := DefaultBootstrapConfig[key.Key8, kadtest.StrAddr]() - require.NoError(t, cfg.Validate()) - }) - - t.Run("clock is not nil", func(t *testing.T) { - cfg := DefaultBootstrapConfig[key.Key8, kadtest.StrAddr]() - cfg.Clock = nil - require.Error(t, cfg.Validate()) - }) - - t.Run("timeout positive", func(t *testing.T) { - cfg := DefaultBootstrapConfig[key.Key8, kadtest.StrAddr]() - cfg.Timeout = 0 - require.Error(t, cfg.Validate()) - cfg.Timeout = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("request concurrency positive", func(t *testing.T) { - cfg := DefaultBootstrapConfig[key.Key8, kadtest.StrAddr]() - cfg.RequestConcurrency = 0 - require.Error(t, cfg.Validate()) - cfg.RequestConcurrency = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("request timeout positive", func(t *testing.T) { - cfg := DefaultBootstrapConfig[key.Key8, kadtest.StrAddr]() - cfg.RequestTimeout = 0 - require.Error(t, cfg.Validate()) - cfg.RequestTimeout = -1 - require.Error(t, cfg.Validate()) - }) -} - -func TestBootstrapStartsIdle(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultBootstrapConfig[key.Key8, kadtest.StrAddr]() - cfg.Clock = clk - - self := kadtest.NewID(key.Key8(0)) - bs, err := NewBootstrap[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - state := bs.Advance(ctx, &EventBootstrapPoll{}) - require.IsType(t, &StateBootstrapIdle{}, state) -} - -func TestBootstrapStart(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultBootstrapConfig[key.Key8, kadtest.StrAddr]() - cfg.Clock = clk - - self := kadtest.NewID(key.Key8(0)) - bs, err := NewBootstrap[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - - msg := kadtest.NewRequest("1", self.Key()) - protocolID := address.ProtocolID("testprotocol") - - // start the bootstrap - state := bs.Advance(ctx, &EventBootstrapStart[key.Key8, kadtest.StrAddr]{ - ProtocolID: protocolID, - Message: msg, - KnownClosestNodes: []kad.NodeID[key.Key8]{a}, - }) - require.IsType(t, &StateBootstrapMessage[key.Key8, kadtest.StrAddr]{}, state) - - // the query should attempt to contact the node it was given - st := state.(*StateBootstrapMessage[key.Key8, kadtest.StrAddr]) - - // the query should be the one just added - require.Equal(t, query.QueryID("bootstrap"), st.QueryID) - - // the query should attempt to contact the node it was given - require.Equal(t, a, st.NodeID) - - // with the correct protocol ID - require.Equal(t, protocolID, st.ProtocolID) - - // with the correct message - require.Equal(t, msg, st.Message) - - // now the bootstrap reports that it is waiting - state = bs.Advance(ctx, &EventBootstrapPoll{}) - require.IsType(t, &StateBootstrapWaiting{}, state) -} - -func TestBootstrapMessageResponse(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultBootstrapConfig[key.Key8, kadtest.StrAddr]() - cfg.Clock = clk - - self := kadtest.NewID(key.Key8(0)) - bs, err := NewBootstrap[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - - msg := kadtest.NewRequest("1", self.Key()) - protocolID := address.ProtocolID("testprotocol") - - // start the bootstrap - state := bs.Advance(ctx, &EventBootstrapStart[key.Key8, kadtest.StrAddr]{ - ProtocolID: protocolID, - Message: msg, - KnownClosestNodes: []kad.NodeID[key.Key8]{a}, - }) - require.IsType(t, &StateBootstrapMessage[key.Key8, kadtest.StrAddr]{}, state) - - // the bootstrap should attempt to contact the node it was given - st := state.(*StateBootstrapMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, query.QueryID("bootstrap"), st.QueryID) - require.Equal(t, a, st.NodeID) - - // notify bootstrap that node was contacted successfully, but no closer nodes - state = bs.Advance(ctx, &EventBootstrapMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: a, - }) - - // bootstrap should respond that its query has finished - require.IsType(t, &StateBootstrapFinished{}, state) - - stf := state.(*StateBootstrapFinished) - require.Equal(t, 1, stf.Stats.Requests) - require.Equal(t, 1, stf.Stats.Success) -} - -func TestBootstrapProgress(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultBootstrapConfig[key.Key8, kadtest.StrAddr]() - cfg.Clock = clk - cfg.RequestConcurrency = 3 // 1 less than the 4 nodes to be visited - - self := kadtest.NewID(key.Key8(0)) - bs, err := NewBootstrap[key.Key8, kadtest.StrAddr](self, cfg) - require.NoError(t, err) - - a := kadtest.NewID(key.Key8(0b00000100)) // 4 - b := kadtest.NewID(key.Key8(0b00001000)) // 8 - c := kadtest.NewID(key.Key8(0b00010000)) // 16 - d := kadtest.NewID(key.Key8(0b00100000)) // 32 - - // ensure the order of the known nodes - require.True(t, self.Key().Xor(a.Key()).Compare(self.Key().Xor(b.Key())) == -1) - require.True(t, self.Key().Xor(b.Key()).Compare(self.Key().Xor(c.Key())) == -1) - require.True(t, self.Key().Xor(c.Key()).Compare(self.Key().Xor(d.Key())) == -1) - - msg := kadtest.NewRequest("1", self.Key()) - protocolID := address.ProtocolID("testprotocol") - - // start the bootstrap - state := bs.Advance(ctx, &EventBootstrapStart[key.Key8, kadtest.StrAddr]{ - ProtocolID: protocolID, - Message: msg, - KnownClosestNodes: []kad.NodeID[key.Key8]{d, a, b, c}, - }) - - // the bootstrap should attempt to contact the closest node it was given - require.IsType(t, &StateBootstrapMessage[key.Key8, kadtest.StrAddr]{}, state) - st := state.(*StateBootstrapMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, query.QueryID("bootstrap"), st.QueryID) - require.Equal(t, a, st.NodeID) - - // next the bootstrap attempts to contact second nearest node - state = bs.Advance(ctx, &EventBootstrapPoll{}) - require.IsType(t, &StateBootstrapMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateBootstrapMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, b, st.NodeID) - - // next the bootstrap attempts to contact third nearest node - state = bs.Advance(ctx, &EventBootstrapPoll{}) - require.IsType(t, &StateBootstrapMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateBootstrapMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, c, st.NodeID) - - // now the bootstrap should be waiting since it is at request capacity - state = bs.Advance(ctx, &EventBootstrapPoll{}) - require.IsType(t, &StateBootstrapWaiting{}, state) - - // notify bootstrap that node was contacted successfully, but no closer nodes - state = bs.Advance(ctx, &EventBootstrapMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: a, - }) - - // now the bootstrap has capacity to contact fourth nearest node - require.IsType(t, &StateBootstrapMessage[key.Key8, kadtest.StrAddr]{}, state) - st = state.(*StateBootstrapMessage[key.Key8, kadtest.StrAddr]) - require.Equal(t, d, st.NodeID) - - // notify bootstrap that a node was contacted successfully - state = bs.Advance(ctx, &EventBootstrapMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: b, - }) - - // bootstrap should respond that it is waiting for messages - require.IsType(t, &StateBootstrapWaiting{}, state) - - // notify bootstrap that a node was contacted successfully - state = bs.Advance(ctx, &EventBootstrapMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: c, - }) - - // bootstrap should respond that it is waiting for last message - require.IsType(t, &StateBootstrapWaiting{}, state) - - // notify bootstrap that the final node was contacted successfully - state = bs.Advance(ctx, &EventBootstrapMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeID: d, - }) - - // bootstrap should respond that its query has finished - require.IsType(t, &StateBootstrapFinished{}, state) - - stf := state.(*StateBootstrapFinished) - require.Equal(t, 4, stf.Stats.Requests) - require.Equal(t, 4, stf.Stats.Success) -} diff --git a/routing/include.go b/routing/include.go deleted file mode 100644 index cab122a..0000000 --- a/routing/include.go +++ /dev/null @@ -1,292 +0,0 @@ -package routing - -import ( - "context" - "fmt" - "time" - - "github.com/benbjohnson/clock" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/kaderr" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/util" -) - -type check[K kad.Key[K], A kad.Address[A]] struct { - NodeInfo kad.NodeInfo[K, A] - Started time.Time -} - -type Include[K kad.Key[K], A kad.Address[A]] struct { - rt kad.RoutingTable[K, kad.NodeID[K]] - - // checks is an index of checks in progress - checks map[string]check[K, A] - - candidates *nodeQueue[K, A] - - // cfg is a copy of the optional configuration supplied to the Include - cfg IncludeConfig -} - -// IncludeConfig specifies optional configuration for an Include -type IncludeConfig struct { - QueueCapacity int // the maximum number of nodes that can be in the candidate queue - Concurrency int // the maximum number of include checks that may be in progress at any one time - Timeout time.Duration // the time to wait before terminating a check that is not making progress - Clock clock.Clock // a clock that may replaced by a mock when testing -} - -// Validate checks the configuration options and returns an error if any have invalid values. -func (cfg *IncludeConfig) Validate() error { - if cfg.Clock == nil { - return &kaderr.ConfigurationError{ - Component: "IncludeConfig", - Err: fmt.Errorf("clock must not be nil"), - } - } - - if cfg.Concurrency < 1 { - return &kaderr.ConfigurationError{ - Component: "IncludeConfig", - Err: fmt.Errorf("concurrency must be greater than zero"), - } - } - - if cfg.Timeout < 1 { - return &kaderr.ConfigurationError{ - Component: "IncludeConfig", - Err: fmt.Errorf("timeout must be greater than zero"), - } - } - - if cfg.QueueCapacity < 1 { - return &kaderr.ConfigurationError{ - Component: "IncludeConfig", - Err: fmt.Errorf("queue size must be greater than zero"), - } - } - - return nil -} - -// DefaultIncludeConfig returns the default configuration options for an Include. -// Options may be overridden before passing to NewInclude -func DefaultIncludeConfig() *IncludeConfig { - return &IncludeConfig{ - Clock: clock.New(), // use standard time - Concurrency: 3, - Timeout: time.Minute, - QueueCapacity: 128, - } -} - -func NewInclude[K kad.Key[K], A kad.Address[A]](rt kad.RoutingTable[K, kad.NodeID[K]], cfg *IncludeConfig) (*Include[K, A], error) { - if cfg == nil { - cfg = DefaultIncludeConfig() - } else if err := cfg.Validate(); err != nil { - return nil, err - } - - return &Include[K, A]{ - candidates: newNodeQueue[K, A](cfg.QueueCapacity), - cfg: *cfg, - rt: rt, - checks: make(map[string]check[K, A], cfg.Concurrency), - }, nil -} - -// Advance advances the state of the include state machine by attempting to advance its query if running. -func (b *Include[K, A]) Advance(ctx context.Context, ev IncludeEvent) IncludeState { - ctx, span := util.StartSpan(ctx, "Include.Advance") - defer span.End() - - switch tev := ev.(type) { - - case *EventIncludeAddCandidate[K, A]: - // Ignore if already running a check - _, checking := b.checks[key.HexString(tev.NodeInfo.ID().Key())] - if checking { - break - } - - // Ignore if node already in routing table - if _, exists := b.rt.GetNode(tev.NodeInfo.ID().Key()); exists { - break - } - - // TODO: potentially time out a check and make room in the queue - if !b.candidates.HasCapacity() { - return &StateIncludeWaitingFull{} - } - b.candidates.Enqueue(ctx, tev.NodeInfo) - - case *EventIncludeMessageResponse[K, A]: - ch, ok := b.checks[key.HexString(tev.NodeInfo.ID().Key())] - if ok { - delete(b.checks, key.HexString(tev.NodeInfo.ID().Key())) - // require that the node responded with at least one closer node - if tev.Response != nil && len(tev.Response.CloserNodes()) > 0 { - if b.rt.AddNode(tev.NodeInfo.ID()) { - return &StateIncludeRoutingUpdated[K, A]{ - NodeInfo: ch.NodeInfo, - } - } - } - } - case *EventIncludeMessageFailure[K, A]: - delete(b.checks, key.HexString(tev.NodeInfo.ID().Key())) - - case *EventIncludePoll: - // ignore, nothing to do - default: - panic(fmt.Sprintf("unexpected event: %T", tev)) - } - - if len(b.checks) == b.cfg.Concurrency { - if !b.candidates.HasCapacity() { - return &StateIncludeWaitingFull{} - } - return &StateIncludeWaitingAtCapacity{} - } - - candidate, ok := b.candidates.Dequeue(ctx) - if !ok { - // No candidate in queue - if len(b.checks) > 0 { - return &StateIncludeWaitingWithCapacity{} - } - return &StateIncludeIdle{} - } - - b.checks[key.HexString(candidate.ID().Key())] = check[K, A]{ - NodeInfo: candidate, - Started: b.cfg.Clock.Now(), - } - - // Ask the node to find itself - return &StateIncludeFindNodeMessage[K, A]{ - NodeInfo: candidate, - } -} - -// nodeQueue is a bounded queue of unique NodeIDs -type nodeQueue[K kad.Key[K], A kad.Address[A]] struct { - capacity int - nodes []kad.NodeInfo[K, A] - keys map[string]struct{} -} - -func newNodeQueue[K kad.Key[K], A kad.Address[A]](capacity int) *nodeQueue[K, A] { - return &nodeQueue[K, A]{ - capacity: capacity, - nodes: make([]kad.NodeInfo[K, A], 0, capacity), - keys: make(map[string]struct{}, capacity), - } -} - -// Enqueue adds a node to the queue. It returns true if the node was -// added and false otherwise. -func (q *nodeQueue[K, A]) Enqueue(ctx context.Context, n kad.NodeInfo[K, A]) bool { - if len(q.nodes) == q.capacity { - return false - } - - if _, exists := q.keys[key.HexString(n.ID().Key())]; exists { - return false - } - - q.nodes = append(q.nodes, n) - q.keys[key.HexString(n.ID().Key())] = struct{}{} - return true -} - -// Dequeue reads an node from the queue. It returns the node and a true value -// if a node was read or nil and false if no node was read. -func (q *nodeQueue[K, A]) Dequeue(ctx context.Context) (kad.NodeInfo[K, A], bool) { - if len(q.nodes) == 0 { - var v kad.NodeInfo[K, A] - return v, false - } - - var n kad.NodeInfo[K, A] - n, q.nodes = q.nodes[0], q.nodes[1:] - delete(q.keys, key.HexString(n.ID().Key())) - - return n, true -} - -func (q *nodeQueue[K, A]) HasCapacity() bool { - return len(q.nodes) < q.capacity -} - -// IncludeState is the state of a include. -type IncludeState interface { - includeState() -} - -// StateIncludeFindNodeMessage indicates that the include subsystem is waiting to send a find node message a node. -// A find node message should be sent to the node, with the target being the node's key. -type StateIncludeFindNodeMessage[K kad.Key[K], A kad.Address[A]] struct { - NodeInfo kad.NodeInfo[K, A] // the node to send the mssage to -} - -// StateIncludeIdle indicates that the include is not running its query. -type StateIncludeIdle struct{} - -// StateIncludeWaitingAtCapacity indicates that the include subsystem is waiting for responses for checks and -// that the maximum number of concurrent checks has been reached. -type StateIncludeWaitingAtCapacity struct{} - -// StateIncludeWaitingWithCapacity indicates that the include subsystem is waiting for responses for checks -// but has capacity to perform more. -type StateIncludeWaitingWithCapacity struct{} - -// StateIncludeWaitingFull indicates that the include subsystem is waiting for responses for checks and -// that the maximum number of queued candidates has been reached. -type StateIncludeWaitingFull struct{} - -// StateIncludeRoutingUpdated indicates the routing table has been updated with a new node. -type StateIncludeRoutingUpdated[K kad.Key[K], A kad.Address[A]] struct { - NodeInfo kad.NodeInfo[K, A] -} - -// includeState() ensures that only Include states can be assigned to an IncludeState. -func (*StateIncludeFindNodeMessage[K, A]) includeState() {} -func (*StateIncludeIdle) includeState() {} -func (*StateIncludeWaitingAtCapacity) includeState() {} -func (*StateIncludeWaitingWithCapacity) includeState() {} -func (*StateIncludeWaitingFull) includeState() {} -func (*StateIncludeRoutingUpdated[K, A]) includeState() {} - -// IncludeEvent is an event intended to advance the state of a include. -type IncludeEvent interface { - includeEvent() -} - -// EventIncludePoll is an event that signals the include that it can perform housekeeping work such as time out queries. -type EventIncludePoll struct{} - -// EventIncludeAddCandidate notifies that a node should be added to the candidate list -type EventIncludeAddCandidate[K kad.Key[K], A kad.Address[A]] struct { - NodeInfo kad.NodeInfo[K, A] // the candidate node -} - -// EventIncludeMessageResponse notifies a include that a sent message has received a successful response. -type EventIncludeMessageResponse[K kad.Key[K], A kad.Address[A]] struct { - NodeInfo kad.NodeInfo[K, A] // the node the message was sent to - Response kad.Response[K, A] // the message response sent by the node -} - -// EventIncludeMessageFailure notifiesa include that an attempt to send a message has failed. -type EventIncludeMessageFailure[K kad.Key[K], A kad.Address[A]] struct { - NodeInfo kad.NodeInfo[K, A] // the node the message was sent to - Error error // the error that caused the failure, if any -} - -// includeEvent() ensures that only Include events can be assigned to the IncludeEvent interface. -func (*EventIncludePoll) includeEvent() {} -func (*EventIncludeAddCandidate[K, A]) includeEvent() {} -func (*EventIncludeMessageResponse[K, A]) includeEvent() {} -func (*EventIncludeMessageFailure[K, A]) includeEvent() {} diff --git a/routing/include_test.go b/routing/include_test.go deleted file mode 100644 index dc2c88c..0000000 --- a/routing/include_test.go +++ /dev/null @@ -1,350 +0,0 @@ -package routing - -import ( - "context" - "testing" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/routing/simplert" -) - -func TestIncludeConfigValidate(t *testing.T) { - t.Run("default is valid", func(t *testing.T) { - cfg := DefaultIncludeConfig() - require.NoError(t, cfg.Validate()) - }) - - t.Run("clock is not nil", func(t *testing.T) { - cfg := DefaultIncludeConfig() - cfg.Clock = nil - require.Error(t, cfg.Validate()) - }) - - t.Run("timeout positive", func(t *testing.T) { - cfg := DefaultIncludeConfig() - cfg.Timeout = 0 - require.Error(t, cfg.Validate()) - cfg.Timeout = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("request concurrency positive", func(t *testing.T) { - cfg := DefaultIncludeConfig() - cfg.Concurrency = 0 - require.Error(t, cfg.Validate()) - cfg.Concurrency = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("queue size positive", func(t *testing.T) { - cfg := DefaultIncludeConfig() - cfg.QueueCapacity = 0 - require.Error(t, cfg.Validate()) - cfg.QueueCapacity = -1 - require.Error(t, cfg.Validate()) - }) -} - -func TestIncludeStartsIdle(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultIncludeConfig() - cfg.Clock = clk - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - - bs, err := NewInclude[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - state := bs.Advance(ctx, &EventIncludePoll{}) - require.IsType(t, &StateIncludeIdle{}, state) -} - -func TestIncludeAddCandidateStartsCheckIfCapacity(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultIncludeConfig() - cfg.Clock = clk - cfg.Concurrency = 1 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - - p, err := NewInclude[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - candidate := kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000100)), - []kadtest.StrAddr{kadtest.StrAddr("4")}, - ) - - // add a candidate - state := p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: candidate, - }) - // the state machine should attempt to send a message - require.IsType(t, &StateIncludeFindNodeMessage[key.Key8, kadtest.StrAddr]{}, state) - - st := state.(*StateIncludeFindNodeMessage[key.Key8, kadtest.StrAddr]) - - // the message should be sent to the candidate node - require.Equal(t, candidate, st.NodeInfo) - - // the message should be looking for the candidate node - require.Equal(t, candidate.ID(), st.NodeInfo.ID()) - - // now the include reports that it is waiting since concurrency is 1 - state = p.Advance(ctx, &EventIncludePoll{}) - require.IsType(t, &StateIncludeWaitingAtCapacity{}, state) -} - -func TestIncludeAddCandidateReportsCapacity(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultIncludeConfig() - cfg.Clock = clk - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - p, err := NewInclude[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - candidate := kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000100)), - []kadtest.StrAddr{kadtest.StrAddr("4")}, - ) - - // add a candidate - state := p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: candidate, - }) - require.IsType(t, &StateIncludeFindNodeMessage[key.Key8, kadtest.StrAddr]{}, state) - - // now the state machine reports that it is waiting with capacity since concurrency - // is greater than the number of checks in flight - state = p.Advance(ctx, &EventIncludePoll{}) - require.IsType(t, &StateIncludeWaitingWithCapacity{}, state) -} - -func TestIncludeAddCandidateOverQueueLength(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultIncludeConfig() - cfg.Clock = clk - cfg.QueueCapacity = 2 // only allow two candidates in the queue - cfg.Concurrency = 3 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - - p, err := NewInclude[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // add a candidate - state := p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000100)), - []kadtest.StrAddr{kadtest.StrAddr("4")}, - ), - }) - require.IsType(t, &StateIncludeFindNodeMessage[key.Key8, kadtest.StrAddr]{}, state) - - // include reports that it is waiting and has capacity for more - state = p.Advance(ctx, &EventIncludePoll{}) - require.IsType(t, &StateIncludeWaitingWithCapacity{}, state) - - // add second candidate - state = p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000010)), - []kadtest.StrAddr{kadtest.StrAddr("2")}, - ), - }) - // sends a message to the candidate - require.IsType(t, &StateIncludeFindNodeMessage[key.Key8, kadtest.StrAddr]{}, state) - - // include reports that it is waiting and has capacity for more - state = p.Advance(ctx, &EventIncludePoll{}) - // sends a message to the candidate - require.IsType(t, &StateIncludeWaitingWithCapacity{}, state) - - // add third candidate - state = p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000011)), - []kadtest.StrAddr{kadtest.StrAddr("3")}, - ), - }) - // sends a message to the candidate - require.IsType(t, &StateIncludeFindNodeMessage[key.Key8, kadtest.StrAddr]{}, state) - - // include reports that it is waiting at capacity since 3 messages are in flight - state = p.Advance(ctx, &EventIncludePoll{}) - require.IsType(t, &StateIncludeWaitingAtCapacity{}, state) - - // add fourth candidate - state = p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000101)), - []kadtest.StrAddr{kadtest.StrAddr("5")}, - ), - }) - - // include reports that it is waiting at capacity since 3 messages are already in flight - require.IsType(t, &StateIncludeWaitingAtCapacity{}, state) - - // add fifth candidate - state = p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000110)), - []kadtest.StrAddr{kadtest.StrAddr("6")}, - ), - }) - - // include reports that it is waiting and the candidate queue is full since it - // is configured to have 3 concurrent checks and 2 queued - require.IsType(t, &StateIncludeWaitingFull{}, state) - - // add sixth candidate - state = p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000111)), - []kadtest.StrAddr{kadtest.StrAddr("7")}, - ), - }) - - // include reports that it is still waiting and the candidate queue is full since it - // is configured to have 3 concurrent checks and 2 queued - require.IsType(t, &StateIncludeWaitingFull{}, state) -} - -func TestIncludeMessageResponse(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultIncludeConfig() - cfg.Clock = clk - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - - p, err := NewInclude[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // add a candidate - state := p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000100)), - []kadtest.StrAddr{kadtest.StrAddr("4")}, - ), - }) - require.IsType(t, &StateIncludeFindNodeMessage[key.Key8, kadtest.StrAddr]{}, state) - - // notify that node was contacted successfully, with no closer nodes - state = p.Advance(ctx, &EventIncludeMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000100)), - []kadtest.StrAddr{kadtest.StrAddr("4")}, - ), - Response: kadtest.NewResponse("resp", []kad.NodeInfo[key.Key8, kadtest.StrAddr]{ - kadtest.NewInfo(kadtest.NewID(key.Key8(4)), []kadtest.StrAddr{"addr_4"}), - kadtest.NewInfo(kadtest.NewID(key.Key8(6)), []kadtest.StrAddr{"addr_6"}), - }), - }) - - // should respond that the routing table was updated - require.IsType(t, &StateIncludeRoutingUpdated[key.Key8, kadtest.StrAddr]{}, state) - - st := state.(*StateIncludeRoutingUpdated[key.Key8, kadtest.StrAddr]) - - // the update is for the correct node - require.Equal(t, kadtest.NewID(key.Key8(4)), st.NodeInfo.ID()) - - // the routing table should contain the node - foundNode, found := rt.GetNode(key.Key8(4)) - require.True(t, found) - require.NotNil(t, foundNode) - - require.True(t, key.Equal(foundNode.Key(), key.Key8(4))) - - // advancing again should reports that it is idle - state = p.Advance(ctx, &EventIncludePoll{}) - require.IsType(t, &StateIncludeIdle{}, state) -} - -func TestIncludeMessageResponseInvalid(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultIncludeConfig() - cfg.Clock = clk - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - - p, err := NewInclude[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // add a candidate - state := p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000100)), - []kadtest.StrAddr{kadtest.StrAddr("4")}, - ), - }) - require.IsType(t, &StateIncludeFindNodeMessage[key.Key8, kadtest.StrAddr]{}, state) - - // notify that node was contacted successfully, but no closer nodes - state = p.Advance(ctx, &EventIncludeMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000100)), - []kadtest.StrAddr{kadtest.StrAddr("4")}, - ), - }) - // should respond that state machine is idle - require.IsType(t, &StateIncludeIdle{}, state) - - // the routing table should not contain the node - foundNode, found := rt.GetNode(key.Key8(4)) - require.False(t, found) - require.Nil(t, foundNode) -} - -func TestIncludeMessageFailure(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultIncludeConfig() - cfg.Clock = clk - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - - p, err := NewInclude[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // add a candidate - state := p.Advance(ctx, &EventIncludeAddCandidate[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000100)), - []kadtest.StrAddr{kadtest.StrAddr("4")}, - ), - }) - require.IsType(t, &StateIncludeFindNodeMessage[key.Key8, kadtest.StrAddr]{}, state) - - // notify that node was not contacted successfully - state = p.Advance(ctx, &EventIncludeMessageFailure[key.Key8, kadtest.StrAddr]{ - NodeInfo: kadtest.NewInfo( - kadtest.NewID(key.Key8(0b00000100)), - []kadtest.StrAddr{kadtest.StrAddr("4")}, - ), - }) - - // should respond that state machine is idle - require.IsType(t, &StateIncludeIdle{}, state) - - // the routing table should not contain the node - foundNode, found := rt.GetNode(key.Key8(4)) - require.False(t, found) - require.Nil(t, foundNode) -} diff --git a/routing/probe.go b/routing/probe.go deleted file mode 100644 index cc755be..0000000 --- a/routing/probe.go +++ /dev/null @@ -1,507 +0,0 @@ -package routing - -import ( - "container/heap" - "context" - "errors" - "fmt" - "time" - - "github.com/benbjohnson/clock" - "go.opentelemetry.io/otel/attribute" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/kaderr" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/util" -) - -type RoutingTableCpl[K kad.Key[K], N kad.NodeID[K]] interface { - kad.RoutingTable[K, N] - - // Cpl returns the longest common prefix length the supplied key shares with the table's key. - Cpl(kk K) int - - // CplSize returns the number of nodes in the table whose longest common prefix with the table's key is of length cpl. - CplSize(cpl int) int -} - -// The Probe state machine performs regular connectivity checks for nodes in a routing table. -// -// The state machine is notified of a new entry in the routing table via the [EventProbeAdd] event. This adds the node -// to an internal list and sets a time for a check to be performed, based on the current time plus a configurable -// interval. -// -// Connectivity checks are performed in time order, so older nodes are processed first. The connectivity check performed -// is the same as for the [Include] state machine: ask the node for closest nodes to itself and confirm that the node -// returns at least one node in the list of closer nodes. The state machine emits the [StateProbeConnectivityCheck] -// state when it wants to check the status of a node. -// -// The state machine expects to be notified either with the [EventProbeMessageResponse] or the -// [EventProbeMessageFailure] events to determine the outcome of the check. If neither are received within a -// configurable timeout the node is marked as failed. -// -// Nodes that receive a successful response have their next check time updated to the current time plus the configured -// [ProbeConfig.CheckInterval]. -// -// Nodes that fail a connectivity check, or are timed out, are removed from the routing table and from the list of nodes -// to check. The state machine emits the [StateProbeNodeFailure] state to notify callers of this event. -// -// The state machine accepts a [EventProbePoll] event to check for outstanding work such as initiating a new check or -// timing out an existing one. -// -// The [EventProbeRemove] event may be used to remove a node from the check list and from the routing table. -// -// The state machine accepts the [EventProbeNotifyConnectivity] event as a notification that an external system has -// performed a suitable connectivity check, such as when the node responds to a query. The probe state machine treats -// these events as if a successful response had been received from a check by advancing the time of the next check. -type Probe[K kad.Key[K], A kad.Address[A]] struct { - rt RoutingTableCpl[K, kad.NodeID[K]] - - // nvl is a list of nodes with information about their connectivity checks - // TODO: this will be expanded with more general scoring information related to their utility - nvl *nodeValueList[K] - - // cfg is a copy of the optional configuration supplied to the Probe - cfg ProbeConfig -} - -// ProbeConfig specifies optional configuration for a Probe -type ProbeConfig struct { - CheckInterval time.Duration // the minimum time interval between checks for a node - Concurrency int // the maximum number of probe checks that may be in progress at any one time - Timeout time.Duration // the time to wait before terminating a check that is not making progress - Clock clock.Clock // a clock that may be replaced by a mock when testing -} - -// Validate checks the configuration options and returns an error if any have invalid values. -func (cfg *ProbeConfig) Validate() error { - if cfg.Clock == nil { - return &kaderr.ConfigurationError{ - Component: "ProbeConfig", - Err: fmt.Errorf("clock must not be nil"), - } - } - - if cfg.Concurrency < 1 { - return &kaderr.ConfigurationError{ - Component: "ProbeConfig", - Err: fmt.Errorf("concurrency must be greater than zero"), - } - } - - if cfg.Timeout < 1 { - return &kaderr.ConfigurationError{ - Component: "ProbeConfig", - Err: fmt.Errorf("timeout must be greater than zero"), - } - } - - if cfg.CheckInterval < 1 { - return &kaderr.ConfigurationError{ - Component: "ProbeConfig", - Err: fmt.Errorf("revisit interval must be greater than zero"), - } - } - - return nil -} - -// DefaultProbeConfig returns the default configuration options for a Probe. -// Options may be overridden before passing to NewProbe -func DefaultProbeConfig() *ProbeConfig { - return &ProbeConfig{ - Clock: clock.New(), // use standard time - Concurrency: 3, // MAGIC - Timeout: time.Minute, // MAGIC - CheckInterval: 6 * time.Hour, // MAGIC - } -} - -func NewProbe[K kad.Key[K], A kad.Address[A]](rt RoutingTableCpl[K, kad.NodeID[K]], cfg *ProbeConfig) (*Probe[K, A], error) { - if cfg == nil { - cfg = DefaultProbeConfig() - } else if err := cfg.Validate(); err != nil { - return nil, err - } - - return &Probe[K, A]{ - cfg: *cfg, - rt: rt, - nvl: NewNodeValueList[K](), - }, nil -} - -// Advance advances the state of the probe state machine by attempting to advance its query if running. -func (p *Probe[K, A]) Advance(ctx context.Context, ev ProbeEvent) ProbeState { - _, span := util.StartSpan(ctx, "Probe.Advance") - defer span.End() - - switch tev := ev.(type) { - case *EventProbePoll: - // ignore, nothing to do - span.SetAttributes(attribute.String("event", "EventProbePoll")) - case *EventProbeAdd[K]: - // check presence in routing table - span.SetAttributes(attribute.String("event", "EventProbeAdd"), attribute.String("nodeid", tev.NodeID.String())) - if _, found := p.rt.GetNode(tev.NodeID.Key()); !found { - // ignore if not in routing table - span.RecordError(errors.New("node not in routing table")) - break - } - - // add a node to the value list - nv := &nodeValue[K]{ - NodeID: tev.NodeID, - NextCheckDue: p.cfg.Clock.Now().Add(p.cfg.CheckInterval), - Cpl: p.rt.Cpl(tev.NodeID.Key()), - } - // TODO: if node was in ongoing list return a state that can signal the caller to cancel any prior outbound message - p.nvl.Put(nv) - case *EventProbeRemove[K]: - span.SetAttributes(attribute.String("event", "EventProbeRemove"), attribute.String("nodeid", tev.NodeID.String())) - p.rt.RemoveKey(tev.NodeID.Key()) - p.nvl.Remove(tev.NodeID) - return &StateProbeNodeFailure[K]{ - NodeID: tev.NodeID, - } - case *EventProbeMessageResponse[K, A]: - span.SetAttributes(attribute.String("event", "EventProbeMessageResponse"), attribute.String("nodeid", tev.NodeInfo.ID().String())) - nv, found := p.nvl.Get(tev.NodeInfo.ID()) - if !found { - // ignore message for unknown node, which might have been removed - span.RecordError(errors.New("node not in node value list")) - break - } - // update next check time - nv.NextCheckDue = p.cfg.Clock.Now().Add(p.cfg.CheckInterval) - - // put into list, which will clear any ongoing check too - p.nvl.Put(nv) - - case *EventProbeMessageFailure[K, A]: - // probe failed, so remove from routing table and from list - span.SetAttributes(attribute.String("event", "EventProbeMessageFailure"), attribute.String("nodeid", tev.NodeInfo.ID().String())) - span.RecordError(tev.Error) - p.rt.RemoveKey(tev.NodeInfo.ID().Key()) - p.nvl.Remove(tev.NodeInfo.ID()) - return &StateProbeNodeFailure[K]{ - NodeID: tev.NodeInfo.ID(), - } - case *EventProbeNotifyConnectivity[K]: - span.SetAttributes(attribute.String("event", "EventProbeNotifyConnectivity"), attribute.String("nodeid", tev.NodeID.String())) - nv, found := p.nvl.Get(tev.NodeID) - if !found { - // ignore message for unknown node, which might have been removed - break - } - // update next check time - nv.NextCheckDue = p.cfg.Clock.Now().Add(p.cfg.CheckInterval) - - // put into list, which will clear any ongoing check too - p.nvl.Put(nv) - - default: - panic(fmt.Sprintf("unexpected event: %T", tev)) - } - - // Check if there is capacity - if p.cfg.Concurrency <= p.nvl.OngoingCount() { - // see if a check can be timed out to free capacity - candidate, found := p.nvl.FindCheckPastDeadline(p.cfg.Clock.Now()) - if !found { - // nothing suitable for time out - return &StateProbeWaitingAtCapacity{} - } - - // mark the node as failed since it timed out - p.rt.RemoveKey(candidate.Key()) - p.nvl.Remove(candidate) - return &StateProbeNodeFailure[K]{ - NodeID: candidate, - } - - } - - // there is capacity to start a new check - next, ok := p.nvl.PeekNext(p.cfg.Clock.Now()) - if !ok { - if p.nvl.OngoingCount() > 0 { - // waiting for a check but nothing else to do - return &StateProbeWaitingWithCapacity{} - } - // nothing happening and nothing to do - return &StateProbeIdle{} - } - - p.nvl.MarkOngoing(next.NodeID, p.cfg.Clock.Now().Add(p.cfg.Timeout)) - - // Ask the node to find itself - return &StateProbeConnectivityCheck[K]{ - NodeID: next.NodeID, - } -} - -// ProbeState is the state of the [Probe] state machine. -type ProbeState interface { - probeState() -} - -// StateProbeConnectivityCheck indicates that the probe subsystem is waiting to send a connectivity check to a node. -// A find node message should be sent to the node, with the target being the node's key. -type StateProbeConnectivityCheck[K kad.Key[K]] struct { - NodeID kad.NodeID[K] // the node to send the message to -} - -// StateProbeIdle indicates that the probe state machine is not running any checks. -type StateProbeIdle struct{} - -// StateProbeWaitingAtCapacity indicates that the probe state machine is waiting for responses for checks and -// the maximum number of concurrent checks has been reached. -type StateProbeWaitingAtCapacity struct{} - -// StateProbeWaitingWithCapacity indicates that the probe state machine is waiting for responses for checks -// but has capacity to perform more. -type StateProbeWaitingWithCapacity struct{} - -// StateProbeNodeFailure indicates a node has failed a connectivity check been removed from the routing table and the probe list -type StateProbeNodeFailure[K kad.Key[K]] struct { - NodeID kad.NodeID[K] -} - -// probeState() ensures that only Probe states can be assigned to the ProbeState interface. -func (*StateProbeConnectivityCheck[K]) probeState() {} -func (*StateProbeIdle) probeState() {} -func (*StateProbeWaitingAtCapacity) probeState() {} -func (*StateProbeWaitingWithCapacity) probeState() {} -func (*StateProbeNodeFailure[K]) probeState() {} - -// ProbeEvent is an event intended to advance the state of a probe. -type ProbeEvent interface { - probeEvent() -} - -// EventProbePoll is an event that signals the probe that it can perform housekeeping work such as time out queries. -type EventProbePoll struct{} - -// EventProbeAdd notifies a probe that a node should be added to its list of nodes. -type EventProbeAdd[K kad.Key[K]] struct { - NodeID kad.NodeID[K] // the node to be probed -} - -// EventProbeRemove notifies a probe that a node should be removed from its list of nodes and the routing table. -type EventProbeRemove[K kad.Key[K]] struct { - NodeID kad.NodeID[K] // the node to be removed -} - -// EventProbeMessageResponse notifies a probe that a sent message has received a successful response. -type EventProbeMessageResponse[K kad.Key[K], A kad.Address[A]] struct { - NodeInfo kad.NodeInfo[K, A] // the node the message was sent to - Response kad.Response[K, A] // the message response sent by the node -} - -// EventProbeMessageFailure notifiesa probe that an attempt to send a message has failed. -type EventProbeMessageFailure[K kad.Key[K], A kad.Address[A]] struct { - NodeInfo kad.NodeInfo[K, A] // the node the message was sent to - Error error // the error that caused the failure, if any -} - -// EventProbeNotifyConnectivity notifies a probe that a node has confirmed connectivity from another source such as a query. -type EventProbeNotifyConnectivity[K kad.Key[K]] struct { - NodeID kad.NodeID[K] -} - -// probeEvent() ensures that only Probe events can be assigned to the ProbeEvent interface. -func (*EventProbePoll) probeEvent() {} -func (*EventProbeAdd[K]) probeEvent() {} -func (*EventProbeRemove[K]) probeEvent() {} -func (*EventProbeMessageResponse[K, A]) probeEvent() {} -func (*EventProbeMessageFailure[K, A]) probeEvent() {} -func (*EventProbeNotifyConnectivity[K]) probeEvent() {} - -type nodeValue[K kad.Key[K]] struct { - NodeID kad.NodeID[K] - Cpl int // the longest common prefix length the node shares with the routing table's key - NextCheckDue time.Time - CheckDeadline time.Time - Index int // the index of the item in the ordering -} - -type nodeValueEntry[K kad.Key[K]] struct { - nv *nodeValue[K] - index int // the index of the item in the ordering -} - -type nodeValueList[K kad.Key[K]] struct { - nodes map[string]*nodeValueEntry[K] - pending *nodeValuePendingList[K] - // ongoing is a list of nodes with ongoing/in-progress probes, loosely ordered earliest to most recent - ongoing []kad.NodeID[K] -} - -func NewNodeValueList[K kad.Key[K]]() *nodeValueList[K] { - return &nodeValueList[K]{ - nodes: make(map[string]*nodeValueEntry[K]), - ongoing: make([]kad.NodeID[K], 0), - pending: new(nodeValuePendingList[K]), - } -} - -// Put adds a node value to the list, replacing any existing value. -// It is added to the pending list and removed from the ongoing list if it was already present there. -func (l *nodeValueList[K]) Put(nv *nodeValue[K]) { - mk := key.HexString(nv.NodeID.Key()) - nve, exists := l.nodes[mk] - if !exists { - nve = &nodeValueEntry[K]{ - nv: nv, - } - } else { - nve.nv = nv - heap.Remove(l.pending, nve.index) - } - heap.Push(l.pending, nve) - l.nodes[mk] = nve - heap.Fix(l.pending, nve.index) - l.removeFromOngoing(nv.NodeID) -} - -func (l *nodeValueList[K]) Get(n kad.NodeID[K]) (*nodeValue[K], bool) { - mk := key.HexString(n.Key()) - nve, found := l.nodes[mk] - if !found { - return nil, false - } - return nve.nv, true -} - -func (l *nodeValueList[K]) PendingCount() int { - return len(*l.pending) -} - -func (l *nodeValueList[K]) OngoingCount() int { - return len(l.ongoing) -} - -func (l *nodeValueList[K]) NodeCount() int { - return len(l.nodes) -} - -// Put removes a node value from the list, deleting its information. -// It is removed from the pending list andongoing list if it was already present in either. -func (l *nodeValueList[K]) Remove(n kad.NodeID[K]) { - mk := key.HexString(n.Key()) - nve, ok := l.nodes[mk] - if !ok { - return - } - delete(l.nodes, mk) - if nve.index >= 0 { - heap.Remove(l.pending, nve.index) - } - l.removeFromOngoing(n) -} - -// FindCheckPastDeadline looks for the first node in the ongoing list whose deadline is -// before the supplied timestamp. -func (l *nodeValueList[K]) FindCheckPastDeadline(ts time.Time) (kad.NodeID[K], bool) { - // ongoing is in start time order, oldest first - for _, n := range l.ongoing { - mk := key.HexString(n.Key()) - nve, ok := l.nodes[mk] - if !ok { - // somehow the node doesn't exist so this is an obvious candidate for removal - return n, true - } - if !nve.nv.CheckDeadline.After(ts) { - return n, true - } - } - return nil, false -} - -func (l *nodeValueList[K]) removeFromOngoing(n kad.NodeID[K]) { - // ongoing list is expected to be small, so linear search is ok - for i := range l.ongoing { - if key.Equal(n.Key(), l.ongoing[i].Key()) { - if len(l.ongoing) > 1 { - // swap with last entry - l.ongoing[i], l.ongoing[len(l.ongoing)-1] = l.ongoing[len(l.ongoing)-1], l.ongoing[i] - } - // remove last entry - l.ongoing[len(l.ongoing)-1] = nil - l.ongoing = l.ongoing[:len(l.ongoing)-1] - return - } - } -} - -// PeekNext returns the next node that is due a connectivity check without removing it -// from the pending list. -func (l *nodeValueList[K]) PeekNext(ts time.Time) (*nodeValue[K], bool) { - if len(*l.pending) == 0 { - return nil, false - } - - nve := (*l.pending)[0] - - // Is the check due yet? - if nve.nv.NextCheckDue.After(ts) { - return nil, false - } - - return (*l.pending)[0].nv, true -} - -// MarkOngoing marks a node as having an ongoing connectivity check. -// It has no effect if the node is not already present in the list. -func (l *nodeValueList[K]) MarkOngoing(n kad.NodeID[K], deadline time.Time) { - mk := key.HexString(n.Key()) - nve, ok := l.nodes[mk] - if !ok { - return - } - nve.nv.CheckDeadline = deadline - l.nodes[mk] = nve - heap.Remove(l.pending, nve.index) - l.ongoing = append(l.ongoing, nve.nv.NodeID) -} - -// nodeValuePendingList is a min-heap of NodeValue ordered by NextCheckDue -type nodeValuePendingList[K kad.Key[K]] []*nodeValueEntry[K] - -func (o nodeValuePendingList[K]) Len() int { return len(o) } -func (o nodeValuePendingList[K]) Less(i, j int) bool { - // if due times are equal, then sort higher cpls first - if o[i].nv.NextCheckDue.Equal(o[j].nv.NextCheckDue) { - return o[i].nv.Cpl > o[j].nv.Cpl - } - - return o[i].nv.NextCheckDue.Before(o[j].nv.NextCheckDue) -} - -func (o nodeValuePendingList[K]) Swap(i, j int) { - o[i], o[j] = o[j], o[i] - o[i].index = i - o[j].index = j -} - -func (o *nodeValuePendingList[K]) Push(x any) { - n := len(*o) - v := x.(*nodeValueEntry[K]) - v.index = n - *o = append(*o, v) -} - -func (o *nodeValuePendingList[K]) Pop() any { - if len(*o) == 0 { - return nil - } - old := *o - n := len(old) - v := old[n-1] - old[n-1] = nil - v.index = -1 - *o = old[0 : n-1] - return v -} diff --git a/routing/probe_test.go b/routing/probe_test.go deleted file mode 100644 index 4e2987d..0000000 --- a/routing/probe_test.go +++ /dev/null @@ -1,857 +0,0 @@ -package routing - -import ( - "container/heap" - "context" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/routing/simplert" -) - -var _ heap.Interface = (*nodeValuePendingList[key.Key8])(nil) - -type unaddressedNodeInfo[K kad.Key[K], A kad.Address[A]] struct { - NodeID kad.NodeID[K] -} - -func (u unaddressedNodeInfo[K, A]) ID() kad.NodeID[K] { return u.NodeID } -func (u unaddressedNodeInfo[K, A]) Addresses() []A { return nil } - -func TestProbeConfigValidate(t *testing.T) { - t.Run("default is valid", func(t *testing.T) { - cfg := DefaultProbeConfig() - require.NoError(t, cfg.Validate()) - }) - - t.Run("clock is not nil", func(t *testing.T) { - cfg := DefaultProbeConfig() - cfg.Clock = nil - require.Error(t, cfg.Validate()) - }) - - t.Run("timeout positive", func(t *testing.T) { - cfg := DefaultProbeConfig() - cfg.Timeout = 0 - require.Error(t, cfg.Validate()) - cfg.Timeout = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("request concurrency positive", func(t *testing.T) { - cfg := DefaultProbeConfig() - cfg.Concurrency = 0 - require.Error(t, cfg.Validate()) - cfg.Concurrency = -1 - require.Error(t, cfg.Validate()) - }) - - t.Run("revisit interval positive", func(t *testing.T) { - cfg := DefaultProbeConfig() - cfg.CheckInterval = 0 - require.Error(t, cfg.Validate()) - cfg.CheckInterval = -1 - require.Error(t, cfg.Validate()) - }) -} - -func TestProbeStartsIdle(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - cfg := DefaultProbeConfig() - cfg.Clock = clk - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - - bs, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - state := bs.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeIdle{}, state) -} - -func TestProbeAddChecksPresenceInRoutingTable(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - cfg := DefaultProbeConfig() - cfg.Clock = clk - cfg.CheckInterval = 10 * time.Minute - - // Set concurrency to allow one check to run - cfg.Concurrency = 1 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - sm, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // Add node that isn't in routing table - state := sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - require.IsType(t, &StateProbeIdle{}, state) - - // advance time by one revisit interval - clk.Add(cfg.CheckInterval) - - // remains idle since probes aren't run unless node in routing table - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeIdle{}, state) -} - -func TestProbeAddStartsCheckIfCapacity(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - cfg := DefaultProbeConfig() - cfg.Clock = clk - cfg.CheckInterval = 10 * time.Minute - - // Set concurrency to allow one check to run - cfg.Concurrency = 1 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - rt.AddNode(kadtest.NewID(key.Key8(4))) - - sm, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // after adding first node the probe should be idle since the - // connectivity check will be scheduled for the future - state := sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - require.IsType(t, &StateProbeIdle{}, state) - - // remains idle - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeIdle{}, state) - - // advance time by one revisit interval - clk.Add(cfg.CheckInterval) - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - - // the probe state machine should attempt to contact the next node - st := state.(*StateProbeConnectivityCheck[key.Key8]) - - // the connectivity check should be for the right node - require.True(t, key.Equal(key.Key8(4), st.NodeID.Key())) -} - -func TestProbeAddManyStartsChecksIfCapacity(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - cfg := DefaultProbeConfig() - cfg.Clock = clk - cfg.CheckInterval = 10 * time.Minute - - // Set concurrency lower than the number of nodes - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - rt.AddNode(kadtest.NewID(key.Key8(4))) - rt.AddNode(kadtest.NewID(key.Key8(3))) - rt.AddNode(kadtest.NewID(key.Key8(2))) - - sm, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // after adding first node the probe should be idle since the - // connectivity check will be scheduled for the future - state := sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - require.IsType(t, &StateProbeIdle{}, state) - - // after adding second node the probe should still be idle since the - // connectivity check will be scheduled for the future - state = sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(3)), - }) - require.IsType(t, &StateProbeIdle{}, state) - - // after adding third node the probe should still be idle since the - // connectivity check will be scheduled for the future - state = sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(2)), - }) - require.IsType(t, &StateProbeIdle{}, state) - - // advance time by one revisit interval - clk.Add(cfg.CheckInterval) - - // Poll the state machine, it should now attempt to contact a node - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - - // the connectivity check should be for the right node - st := state.(*StateProbeConnectivityCheck[key.Key8]) - require.True(t, key.Equal(key.Key8(4), st.NodeID.Key())) - - // Poll the state machine, it should now attempt to contact another node - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - - // the connectivity check should be for the right node - st = state.(*StateProbeConnectivityCheck[key.Key8]) - require.True(t, key.Equal(key.Key8(2), st.NodeID.Key())) - - // Poll the state machine, it should now be at capacity - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeWaitingAtCapacity{}, state) -} - -func TestProbeAddReportsCapacity(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - cfg := DefaultProbeConfig() - cfg.Clock = clk - cfg.CheckInterval = 10 * time.Minute - - // Set concurrency to allow more than one check to run - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - rt.AddNode(kadtest.NewID(key.Key8(4))) - - sm, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // after adding first node the probe should be idle since the - // connectivity check will be scheduled for the future - state := sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - require.IsType(t, &StateProbeIdle{}, state) - - // remains idle - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeIdle{}, state) - - // advance time by one revisit interval - clk.Add(cfg.CheckInterval) - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - - // the probe state machine should attempt to contact the next node - st := state.(*StateProbeConnectivityCheck[key.Key8]) - - // the connectivity check should be for the right node - require.True(t, key.Equal(key.Key8(4), st.NodeID.Key())) - - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeWaitingWithCapacity{}, state) -} - -func TestProbeRemoveDeletesNodeValue(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - cfg := DefaultProbeConfig() - cfg.Clock = clk - cfg.CheckInterval = 10 * time.Minute - - // Set concurrency to allow more than one check to run - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - rt.AddNode(kadtest.NewID(key.Key8(4))) - - sm, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // after adding first node the probe should be idle since the - // connectivity check will be scheduled for the future - state := sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - require.IsType(t, &StateProbeIdle{}, state) - - // remove the node - state = sm.Advance(ctx, &EventProbeRemove[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - - // state indicate that node failed - require.IsType(t, &StateProbeNodeFailure[key.Key8]{}, state) - - // advance time by one revisit interval - clk.Add(cfg.CheckInterval) - - // state remains idle since there are no nodes to probe - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeIdle{}, state) -} - -func TestNodeValueList(t *testing.T) { - t.Run("put new", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now(), - } - - l.Put(nv) - - got, found := l.Get(kadtest.NewID(key.Key8(4))) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), key.Key8(4))) - }) - - t.Run("put replace before", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv1 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now(), - } - - l.Put(nv1) - - nv2 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now().Add(-time.Minute), - } - l.Put(nv2) - - got, found := l.Get(kadtest.NewID(key.Key8(4))) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), key.Key8(4))) - require.Equal(t, nv2.NextCheckDue, got.NextCheckDue) - }) - - t.Run("put replace after", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv1 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now(), - } - - l.Put(nv1) - - nv2 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now().Add(time.Minute), - } - l.Put(nv2) - - got, found := l.Get(kadtest.NewID(key.Key8(4))) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), key.Key8(4))) - require.Equal(t, nv2.NextCheckDue, got.NextCheckDue) - }) - - t.Run("remove existing", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now(), - } - - l.Put(nv) - - require.Equal(t, 1, l.PendingCount()) - require.Equal(t, 1, l.NodeCount()) - - _, found := l.Get(kadtest.NewID(key.Key8(4))) - require.True(t, found) - - l.Remove(kadtest.NewID(key.Key8(4))) - _, found = l.Get(kadtest.NewID(key.Key8(4))) - require.False(t, found) - - require.Equal(t, 0, l.PendingCount()) - require.Equal(t, 0, l.NodeCount()) - }) - - t.Run("remove not-existing", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now(), - } - - l.Put(nv) - - l.Remove(kadtest.NewID(key.Key8(5))) - _, found := l.Get(kadtest.NewID(key.Key8(4))) - require.True(t, found) - }) - - t.Run("next empty list", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - got, found := l.PeekNext(clk.Now()) - require.False(t, found) - require.Nil(t, got) - }) - - t.Run("next one entry", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now(), - } - l.Put(nv) - - got, found := l.PeekNext(clk.Now()) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), key.Key8(4))) - }) - - t.Run("next sorts by next check due", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv1 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(5)), - NextCheckDue: clk.Now().Add(-time.Minute), - } - l.Put(nv1) - nv2 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now().Add(-2 * time.Minute), - } - l.Put(nv2) - - got, found := l.PeekNext(clk.Now()) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), nv2.NodeID.Key())) - - nv2.NextCheckDue = clk.Now() - l.Put(nv2) - - got, found = l.PeekNext(clk.Now()) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), nv1.NodeID.Key())) - }) - - t.Run("next sorts by cpl descending after time", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv1 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(5)), - Cpl: 1, - NextCheckDue: clk.Now().Add(-time.Minute), - } - l.Put(nv1) - nv2 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - Cpl: 2, - NextCheckDue: clk.Now().Add(-time.Minute), - } - l.Put(nv2) - - got, found := l.PeekNext(clk.Now()) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), nv2.NodeID.Key())) - - nv2.NextCheckDue = clk.Now() - l.Put(nv2) - - got, found = l.PeekNext(clk.Now()) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), nv1.NodeID.Key())) - }) - - t.Run("next not due", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv1 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(5)), - NextCheckDue: clk.Now().Add(time.Minute), - } - l.Put(nv1) - nv2 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now().Add(2 * time.Minute), - } - l.Put(nv2) - - got, found := l.PeekNext(clk.Now()) - require.False(t, found) - require.Nil(t, got) - }) - - t.Run("mark ongoing", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv1 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(5)), - NextCheckDue: clk.Now().Add(time.Minute), - } - l.Put(nv1) - require.Equal(t, 1, l.PendingCount()) - require.Equal(t, 0, l.OngoingCount()) - require.Equal(t, 1, l.NodeCount()) - - l.MarkOngoing(kadtest.NewID(key.Key8(5)), clk.Now().Add(time.Minute)) - require.Equal(t, 0, l.PendingCount()) - require.Equal(t, 1, l.OngoingCount()) - require.Equal(t, 1, l.NodeCount()) - }) - - t.Run("mark ongoing changes next", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv1 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(5)), - NextCheckDue: clk.Now().Add(-2 * time.Minute), - } - l.Put(nv1) - - nv2 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now().Add(-1 * time.Minute), - } - l.Put(nv2) - - require.Equal(t, 2, l.PendingCount()) - require.Equal(t, 0, l.OngoingCount()) - require.Equal(t, 2, l.NodeCount()) - - // nv1 is the next node due - got, found := l.PeekNext(clk.Now()) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), nv1.NodeID.Key())) - - l.MarkOngoing(nv1.NodeID, clk.Now().Add(time.Minute)) - require.Equal(t, 1, l.PendingCount()) - require.Equal(t, 1, l.OngoingCount()) - require.Equal(t, 2, l.NodeCount()) - - // nv2 is now the next node due - got, found = l.PeekNext(clk.Now()) - require.True(t, found) - require.True(t, key.Equal(got.NodeID.Key(), nv2.NodeID.Key())) - }) - - t.Run("put removes from ongoing", func(t *testing.T) { - t.Parallel() - - clk := clock.NewMock() - l := NewNodeValueList[key.Key8]() - nv1 := &nodeValue[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - NextCheckDue: clk.Now(), - } - l.Put(nv1) - - require.Equal(t, 1, l.PendingCount()) - require.Equal(t, 0, l.OngoingCount()) - require.Equal(t, 1, l.NodeCount()) - - l.MarkOngoing(nv1.NodeID, clk.Now().Add(time.Minute)) - - require.Equal(t, 0, l.PendingCount()) - require.Equal(t, 1, l.OngoingCount()) - require.Equal(t, 1, l.NodeCount()) - - l.Put(nv1) - - require.Equal(t, 1, l.PendingCount()) - require.Equal(t, 0, l.OngoingCount()) - require.Equal(t, 1, l.NodeCount()) - }) -} - -func TestProbeMessageResponse(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - cfg := DefaultProbeConfig() - cfg.Clock = clk - cfg.CheckInterval = 10 * time.Minute - - // Set concurrency to allow more than one check to run - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - rt.AddNode(kadtest.NewID(key.Key8(4))) - - sm, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // after adding first node the probe should be idle since the - // connectivity check will be scheduled for the future - state := sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - require.IsType(t, &StateProbeIdle{}, state) - - // advance time by one revisit interval - clk.Add(cfg.CheckInterval) - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - - // the probe state machine should attempt to contact the next node - st := state.(*StateProbeConnectivityCheck[key.Key8]) - - // notify that node was contacted successfully, with no closer nodes - state = sm.Advance(ctx, &EventProbeMessageResponse[key.Key8, kadtest.StrAddr]{ - NodeInfo: unaddressedNodeInfo[key.Key8, kadtest.StrAddr]{ - NodeID: st.NodeID, - }, - Response: kadtest.NewResponse("resp", []kad.NodeInfo[key.Key8, kadtest.StrAddr]{ - kadtest.NewInfo(kadtest.NewID(key.Key8(4)), []kadtest.StrAddr{"addr_4"}), - kadtest.NewInfo(kadtest.NewID(key.Key8(6)), []kadtest.StrAddr{"addr_6"}), - }), - }) - - // node remains in routing table - _, found := rt.GetNode(key.Key8(4)) - require.True(t, found) - - // state machine now idle - require.IsType(t, &StateProbeIdle{}, state) - - // advance time by another revisit interval - clk.Add(cfg.CheckInterval) - - // the probe state machine should attempt to contact node again, now it is time - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - - // the connectivity check should be for the right node - require.True(t, key.Equal(key.Key8(4), st.NodeID.Key())) - - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeWaitingWithCapacity{}, state) -} - -func TestProbeMessageFailure(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - cfg := DefaultProbeConfig() - cfg.Clock = clk - cfg.CheckInterval = 10 * time.Minute - - // Set concurrency to allow more than one check to run - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - rt.AddNode(kadtest.NewID(key.Key8(4))) - - sm, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // after adding first node the probe should be idle since the - // connectivity check will be scheduled for the future - state := sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - require.IsType(t, &StateProbeIdle{}, state) - - // advance time by one revisit interval - clk.Add(cfg.CheckInterval) - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - - // the probe state machine should attempt to contact the next node - st := state.(*StateProbeConnectivityCheck[key.Key8]) - - // notify that node was contacted successfully, with no closer nodes - state = sm.Advance(ctx, &EventProbeMessageFailure[key.Key8, kadtest.StrAddr]{ - NodeInfo: unaddressedNodeInfo[key.Key8, kadtest.StrAddr]{ - NodeID: st.NodeID, - }, - }) - - // state machine announces node failure - require.IsType(t, &StateProbeNodeFailure[key.Key8]{}, state) - stf := state.(*StateProbeNodeFailure[key.Key8]) - - // the failure should be for the right node - require.True(t, key.Equal(key.Key8(4), stf.NodeID.Key())) - - // node has been removed from routing table - _, found := rt.GetNode(key.Key8(4)) - require.False(t, found) - - // advance time by another revisit interval - clk.Add(cfg.CheckInterval) - - // state machine still idle since node was removed - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeIdle{}, state) -} - -func TestProbeNotifyConnectivity(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - cfg := DefaultProbeConfig() - cfg.Clock = clk - cfg.CheckInterval = 10 * time.Minute - cfg.Concurrency = 2 - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - rt.AddNode(kadtest.NewID(key.Key8(4))) - rt.AddNode(kadtest.NewID(key.Key8(3))) - - sm, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // after adding first node the probe should be idle since the - // connectivity check will be scheduled for the future (t0+10) - state := sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - - // not time for a check yet - require.IsType(t, &StateProbeIdle{}, state) - - // advance time by less than the revisit interval - // time is now (t0+2) - clk.Add(2 * time.Minute) - - // add a second node, which will be second in the probe list since it's - // time of next check will be later (t0+2+10=t0+12) - state = sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(3)), - }) - - // still not time for a check - require.IsType(t, &StateProbeIdle{}, state) - - // advance time past the first node's check time but before the second node's - // time is now (t0+2+9=t0+11) - clk.Add(9 * time.Minute) - - // notify that the node with key 4 was connected to successfully by another process - // this will delay the time for the next check to t0+11+10=to+21 - state = sm.Advance(ctx, &EventProbeNotifyConnectivity[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - - // still not time for a check - require.IsType(t, &StateProbeIdle{}, state) - - // advance time past second node's check time - // time is now (t0+2+9+4=t0+15) - clk.Add(4 * time.Minute) - - // Poll the state machine, it should now attempt to contact a node - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - - // the connectivity check should be for the right node, which is the one - // that did not get a connectivity notification - st := state.(*StateProbeConnectivityCheck[key.Key8]) - require.True(t, key.Equal(key.Key8(3), st.NodeID.Key())) - - // Poll the state machine, it should now waiting for a response but still have capacity - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeWaitingWithCapacity{}, state) -} - -func TestProbeTimeout(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - cfg := DefaultProbeConfig() - cfg.Clock = clk - cfg.CheckInterval = 10 * time.Minute - cfg.Timeout = 3 * time.Minute - cfg.Concurrency = 1 // one probe at a time, timeouts will be used to free capacity if there are more requests - - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](kadtest.NewID(key.Key8(128)), 5) - rt.AddNode(kadtest.NewID(key.Key8(4))) - rt.AddNode(kadtest.NewID(key.Key8(3))) - - sm, err := NewProbe[key.Key8, kadtest.StrAddr](rt, cfg) - require.NoError(t, err) - - // add a node - state := sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(4)), - }) - - // not time for a check yet - require.IsType(t, &StateProbeIdle{}, state) - - // advance time a little - clk.Add(time.Minute) - - // add another node - state = sm.Advance(ctx, &EventProbeAdd[key.Key8]{ - NodeID: kadtest.NewID(key.Key8(3)), - }) - - // not time for a check yet - require.IsType(t, &StateProbeIdle{}, state) - - // advance time by check interval - clk.Add(cfg.CheckInterval) - - // poll state machine - state = sm.Advance(ctx, &EventProbePoll{}) - - // the connectivity check should start - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - stm := state.(*StateProbeConnectivityCheck[key.Key8]) - require.True(t, key.Equal(key.Key8(4), stm.NodeID.Key())) - - // Poll the state machine, it should now waiting for a response with no capacity - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeWaitingAtCapacity{}, state) - - // advance time past the timeout - clk.Add(cfg.Timeout) - - // state machine announces node failure - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeNodeFailure[key.Key8]{}, state) - stf := state.(*StateProbeNodeFailure[key.Key8]) - - // the failure should be for the right node - require.True(t, key.Equal(key.Key8(4), stf.NodeID.Key())) - - // node has been removed from routing table - _, found := rt.GetNode(key.Key8(4)) - require.False(t, found) - - // state machine starts check for next node now there is capacity - state = sm.Advance(ctx, &EventProbePoll{}) - require.IsType(t, &StateProbeConnectivityCheck[key.Key8]{}, state) - stm = state.(*StateProbeConnectivityCheck[key.Key8]) - require.True(t, key.Equal(key.Key8(3), stm.NodeID.Key())) -} diff --git a/server/README.md b/server/README.md deleted file mode 100644 index 91feedf..0000000 --- a/server/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Server - -A Kademlia `Server` is responsible for answering requests from remote peers. It implements a single `HandleRequest` function producing a response from the provided request message. - -```go -// Server is the interface for handling requests from remote nodes. -type Server interface { - // HandleRequest handles a request from a remote peer. - HandleRequest(context.Context, kad.NodeID, - message.MinKadMessage) (message.MinKadMessage, error) -} -``` \ No newline at end of file diff --git a/server/basicserver/basicserver.go b/server/basicserver/basicserver.go deleted file mode 100644 index ad944bc..0000000 --- a/server/basicserver/basicserver.go +++ /dev/null @@ -1,121 +0,0 @@ -package basicserver - -import ( - "context" - "time" - - "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/libp2p" - "github.com/plprobelab/go-kademlia/network/endpoint" - "github.com/plprobelab/go-kademlia/sim" - "github.com/plprobelab/go-kademlia/util" -) - -type BasicServer[A kad.Address[A]] struct { - rt kad.RoutingTable[key.Key256, kad.NodeID[key.Key256]] - endpoint endpoint.Endpoint[key.Key256, A] - - peerstoreTTL time.Duration - numberOfCloserPeersToSend int -} - -// var _ server.Server = (*BasicServer)(nil) - -func NewBasicServer[A kad.Address[A]](rt kad.RoutingTable[key.Key256, kad.NodeID[key.Key256]], endpoint endpoint.Endpoint[key.Key256, A], - options ...Option, -) *BasicServer[A] { - var cfg Config - if err := cfg.Apply(append([]Option{DefaultConfig}, options...)...); err != nil { - return nil - } - - return &BasicServer[A]{ - rt: rt, - endpoint: endpoint, - peerstoreTTL: cfg.PeerstoreTTL, - numberOfCloserPeersToSend: cfg.NumberUsefulCloserPeers, - } -} - -func (s *BasicServer[A]) HandleRequest(ctx context.Context, rpeer kad.NodeID[key.Key256], - msg kad.Message, -) (kad.Message, error) { - switch msg := msg.(type) { - case *sim.Message[key.Key256, A]: - return s.HandleFindNodeRequest(ctx, rpeer, msg) - case *libp2p.Message: - switch msg.GetType() { - case libp2p.Message_FIND_NODE: - return s.HandleFindNodeRequest(ctx, rpeer, msg) - default: - return nil, ErrIpfsV1InvalidRequest - } - default: - return nil, ErrUnknownMessageFormat - } -} - -func (s *BasicServer[A]) HandleFindNodeRequest(ctx context.Context, - rpeer kad.NodeID[key.Key256], msg kad.Message, -) (kad.Message, error) { - var target key.Key256 - - switch msg := msg.(type) { - case *sim.Message[key.Key256, A]: - target = msg.Target() - case *libp2p.Message: - p := peer.ID("") - if p.UnmarshalBinary(msg.GetKey()) != nil { - // invalid requested key (not a peer.ID) - return nil, ErrIpfsV1InvalidPeerID - } - t := libp2p.NewPeerID(p) - target = t.Key() - default: - // invalid request, don't reply - return nil, ErrUnknownMessageFormat - } - - _, span := util.StartSpan(ctx, "SimServer.HandleFindNodeRequest", trace.WithAttributes( - attribute.Stringer("Requester", rpeer), - attribute.String("Target", key.HexString(target)))) - defer span.End() - - peers := s.rt.NearestNodes(target, s.numberOfCloserPeersToSend) - - span.AddEvent("Nearest peers", trace.WithAttributes( - attribute.Int("count", len(peers)), - )) - - var resp kad.Message - switch msg.(type) { - case *sim.Message[key.Key256, A]: - peerAddrs := make([]kad.NodeInfo[key.Key256, A], len(peers)) - var index int - for _, p := range peers { - na, err := s.endpoint.NetworkAddress(p) - if err != nil { - span.RecordError(err) - continue - } - peerAddrs[index] = na - index++ - } - resp = sim.NewResponse(peerAddrs[:index]) - case *libp2p.Message: - nEndpoint, ok := s.endpoint.(endpoint.NetworkedEndpoint[key.Key256, multiaddr.Multiaddr]) - if !ok { - span.RecordError(ErrNotNetworkedEndpoint) - return nil, ErrNotNetworkedEndpoint - } - resp = libp2p.FindPeerResponse(peers, nEndpoint) - } - - return resp, nil -} diff --git a/server/basicserver/basicserver_errors.go b/server/basicserver/basicserver_errors.go deleted file mode 100644 index 300a750..0000000 --- a/server/basicserver/basicserver_errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package basicserver - -import "errors" - -var ( - ErrNotNetworkedEndpoint = errors.New("endpoint is not a NetworkedEndpoint") - ErrUnknownMessageFormat = errors.New("unknown message format") - ErrIpfsV1InvalidPeerID = errors.New("IpfsV1 Message contains invalid peer.ID") - ErrIpfsV1InvalidRequest = errors.New("IpfsV1 Message unknown request type") - ErrSimMessageNilTarget = errors.New("SimMessage target is nil") -) diff --git a/server/basicserver/basicserver_options.go b/server/basicserver/basicserver_options.go deleted file mode 100644 index 4bb5910..0000000 --- a/server/basicserver/basicserver_options.go +++ /dev/null @@ -1,49 +0,0 @@ -package basicserver - -import ( - "fmt" - "time" -) - -// Config is a structure containing all the options that can be used when -// constructing a BasicServer. -type Config struct { - PeerstoreTTL time.Duration - NumberUsefulCloserPeers int -} - -// Apply applies the BasicServer options to this Option -func (cfg *Config) Apply(opts ...Option) error { - for i, opt := range opts { - if err := opt(cfg); err != nil { - return fmt.Errorf("BasicServer option %d failed: %s", i, err) - } - } - return nil -} - -// Option type for BasicServer -type Option func(*Config) error - -// DefaultConfig is the default options for BasicServer. This option is always -// prepended to the list of options passed to the BasicServer constructor. -var DefaultConfig = func(cfg *Config) error { - cfg.PeerstoreTTL = 30 * time.Minute - cfg.NumberUsefulCloserPeers = 20 - - return nil -} - -func WithPeerstoreTTL(ttl time.Duration) Option { - return func(cfg *Config) error { - cfg.PeerstoreTTL = ttl - return nil - } -} - -func WithNumberUsefulCloserPeers(n int) Option { - return func(cfg *Config) error { - cfg.NumberUsefulCloserPeers = n - return nil - } -} diff --git a/server/basicserver/basicserver_test.go b/server/basicserver/basicserver_test.go deleted file mode 100644 index 6930d5c..0000000 --- a/server/basicserver/basicserver_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package basicserver - -import ( - "context" - "errors" - "fmt" - "net" - "testing" - "time" - - "github.com/multiformats/go-multiaddr" - "github.com/plprobelab/go-kademlia/libp2p" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" - - "github.com/benbjohnson/clock" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/routing/simplert" - "github.com/plprobelab/go-kademlia/sim" - "github.com/stretchr/testify/require" -) - -// remotePeers with bucket assignments wrt to self -var kadRemotePeers = []kad.NodeInfo[key.Key256, net.IP]{ - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b10001000})), nil), // 1000 1000 (bucket 0) - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b11010010})), nil), // 1101 0010 (bucket 0) - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b01001011})), nil), // 0100 1011 (bucket 1) - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b01010011})), nil), // 0101 0011 (bucket 1) - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b00101110})), nil), // 0010 1110 (bucket 2) - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b00110110})), nil), // 0011 0110 (bucket 2) - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b00011111})), nil), // 0001 1111 (bucket 3) - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b00010001})), nil), // 0001 0001 (bucket 3) - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b00001000})), nil), // 0000 1000 (bucket 4) -} - -func TestSimMessageHandling(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - peerstoreTTL := time.Second // doesn't matter as we use fakeendpoint - numberOfCloserPeersToSend := 4 - - self := kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(key.ZeroKey256()), nil) // 0000 0000 - - router := sim.NewRouter[key.Key256, net.IP]() - sched := event.NewSimpleScheduler(clk) - fakeEndpoint := sim.NewEndpoint[key.Key256, net.IP](self.ID(), sched, router) - rt := simplert.New[key.Key256, kad.NodeID[key.Key256]](self.ID(), 2) - - // add peers to routing table and peerstore - for _, p := range kadRemotePeers { - err := fakeEndpoint.MaybeAddToPeerstore(ctx, p, peerstoreTTL) - require.NoError(t, err) - success := rt.AddNode(p.ID()) - require.True(t, success) - } - - s0 := NewBasicServer[net.IP](rt, fakeEndpoint, WithPeerstoreTTL(peerstoreTTL), - WithNumberUsefulCloserPeers(numberOfCloserPeersToSend)) - - requester := kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b00000001})), nil) // 0000 0001 - fakeEndpoint.MaybeAddToPeerstore(ctx, requester, peerstoreTTL) - - req0 := sim.NewRequest[key.Key256, net.IP](kadtest.Key256WithLeadingBytes([]byte{0b00000000})) - msg, err := s0.HandleRequest(ctx, requester.ID(), req0) - require.NoError(t, err) - - resp, ok := msg.(kad.Response[key.Key256, net.IP]) - require.True(t, ok) - require.Len(t, resp.CloserNodes(), numberOfCloserPeersToSend) - // closer peers should be ordered by distance to 0000 0000 - // [8] 0000 1000, [7] 0001 0001, [6] 0001 1111, [4] 0010 1110 - order := []kad.NodeInfo[key.Key256, net.IP]{ - kadRemotePeers[8], kadRemotePeers[7], - kadRemotePeers[6], kadRemotePeers[4], - } - for i, p := range resp.CloserNodes() { - require.Equal(t, order[i], p) - } - - req1 := sim.NewRequest[key.Key256, net.IP](kadtest.Key256WithLeadingBytes([]byte{0b11111111})) - msg, err = s0.HandleRequest(ctx, requester.ID(), req1) - require.NoError(t, err) - resp, ok = msg.(kad.Response[key.Key256, net.IP]) - require.True(t, ok) - require.Len(t, resp.CloserNodes(), numberOfCloserPeersToSend) - // closer peers should be ordered by distance to 1111 1111 - // [1] 1101 0010, [0] 1000 1000, [3] 0101 0011, [2] 0100 1011 - order = []kad.NodeInfo[key.Key256, net.IP]{ - kadRemotePeers[1], kadRemotePeers[0], - kadRemotePeers[3], kadRemotePeers[2], - } - for i, p := range resp.CloserNodes() { - require.Equal(t, order[i], p) - } - - numberOfCloserPeersToSend = 3 - s1 := NewBasicServer[net.IP](rt, fakeEndpoint, WithNumberUsefulCloserPeers(3)) - - req2 := sim.NewRequest[key.Key256, net.IP](kadtest.Key256WithLeadingBytes([]byte{0b01100000})) - msg, err = s1.HandleRequest(ctx, requester.ID(), req2) - require.NoError(t, err) - resp, ok = msg.(kad.Response[key.Key256, net.IP]) - require.True(t, ok) - require.Len(t, resp.CloserNodes(), numberOfCloserPeersToSend) - // closer peers should be ordered by distance to 0110 0000 - // [2] 0100 1011, [3] 0101 0011, [4] 0010 1110 - order = []kad.NodeInfo[key.Key256, net.IP]{kadRemotePeers[2], kadRemotePeers[3], kadRemotePeers[4]} - for i, p := range resp.CloserNodes() { - require.Equal(t, order[i], p) - } -} - -func TestInvalidSimRequests(t *testing.T) { - ctx := context.Background() - // invalid option - s := NewBasicServer[net.IP](nil, nil, func(*Config) error { - return errors.New("invalid option") - }) - require.Nil(t, s) - - peerstoreTTL := time.Second // doesn't matter as we use fakeendpoint - - clk := clock.New() - router := sim.NewRouter[key.Key256, net.IP]() - - self := kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(key.ZeroKey256()), nil) // 0000 0000 - - // create a valid server - sched := event.NewSimpleScheduler(clk) - fakeEndpoint := sim.NewEndpoint[key.Key256, net.IP](self.ID(), sched, router) - rt := simplert.New[key.Key256, kad.NodeID[key.Key256]](self.ID(), 2) - - // add peers to routing table and peerstore - for _, p := range kadRemotePeers { - err := fakeEndpoint.MaybeAddToPeerstore(ctx, p, peerstoreTTL) - require.NoError(t, err) - success := rt.AddNode(p.ID()) - require.True(t, success) - } - - s = NewBasicServer[net.IP](rt, fakeEndpoint) - require.NotNil(t, s) - - requester := kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0b00000001})) // 0000 0001 - - // invalid message format (not a Message) - req0 := struct{}{} - _, err := s.HandleFindNodeRequest(ctx, requester, req0) - require.Error(t, err) - - // empty request - req1 := &sim.Message[key.Key256, net.IP]{} - s.HandleFindNodeRequest(ctx, requester, req1) - - // request with invalid key (not matching the expected length) - req2 := sim.NewRequest[key.Key32, net.IP](key.Key32(0b00000000000000010000000000000000)) - s.HandleFindNodeRequest(ctx, requester, req2) -} - -func TestSimRequestNoNetworkAddress(t *testing.T) { - ctx := context.Background() - // invalid option - s := NewBasicServer[net.IP](nil, nil, func(*Config) error { - return errors.New("invalid option") - }) - require.Nil(t, s) - - clk := clock.New() - router := sim.NewRouter[key.Key256, net.IP]() - - self := kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0})), nil) // 0000 0000 - - // create a valid server - sched := event.NewSimpleScheduler(clk) - fakeEndpoint := sim.NewEndpoint(self.ID(), sched, router) - rt := simplert.New[key.Key256, kad.NodeID[key.Key256]](self.ID(), 2) - - parsed, err := peer.Decode("1EooooPEER") - require.NoError(t, err) - addrInfo := libp2p.NewAddrInfo(peer.AddrInfo{ - ID: parsed, - Addrs: nil, - }) - - // add peer to routing table, but NOT to peerstore - success := rt.AddNode(addrInfo.ID()) - require.True(t, success) - - s = NewBasicServer[net.IP](rt, fakeEndpoint) - require.NotNil(t, s) - - require.NotNil(t, s) - - requester := kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0x80})) - - // sim request message (for net.IP key) - req := sim.NewRequest[key.Key256, net.IP](requester.Key()) - msg, err := s.HandleFindNodeRequest(ctx, requester, req) - require.NoError(t, err) - resp, ok := msg.(kad.Response[key.Key256, net.IP]) - require.True(t, ok) - fmt.Println(resp.CloserNodes()) - require.Len(t, resp.CloserNodes(), 0) -} - -type invalidEndpoint[K kad.Key[K], A kad.Address[A]] struct{} - -// var _ endpoint.Endpoint = (*invalidEndpoint)(nil) - -func (e *invalidEndpoint[K, A]) MaybeAddToPeerstore(context.Context, kad.NodeInfo[K, A], - time.Duration, -) error { - return nil -} - -func (e *invalidEndpoint[K, A]) SendRequestHandleResponse(context.Context, - address.ProtocolID, kad.NodeID[K], kad.Message, - kad.Message, time.Duration, endpoint.ResponseHandlerFn[K, A], -) error { - return nil -} - -func (e *invalidEndpoint[K, A]) SendMessage(context.Context, address.ProtocolID, kad.NodeID[K], kad.Request[K, A]) (kad.Response[K, A], error) { - return nil, nil -} - -func (e *invalidEndpoint[K, A]) Key() K { - var v K - return v -} - -func (e *invalidEndpoint[K, A]) NetworkAddress(kad.NodeID[K]) (kad.NodeInfo[K, A], error) { - return nil, nil -} - -func TestInvalidIpfsv1Requests(t *testing.T) { - ctx := context.Background() - - peerstoreTTL := time.Second // doesn't matter as we use fakeendpoint - numberOfCloserPeersToSend := 4 - - selfPid, err := peer.Decode("1EooooSELF") - require.NoError(t, err) - self := libp2p.NewPeerID(selfPid) - - invalidEP := &invalidEndpoint[key.Key256, multiaddr.Multiaddr]{} - rt := simplert.New[key.Key256, kad.NodeID[key.Key256]](self, 4) - - nPeers := 6 - peerids := make([]kad.NodeID[key.Key256], nPeers) - - for i := 0; i < nPeers; i++ { - // create peer.ID "1EoooPEER2" until "1EoooPEER7" - p, err := peer.Decode("1EoooPEER" + fmt.Sprint(i+2)) - require.NoError(t, err) - peerids[i] = libp2p.NewPeerID(p) - - // ipfsv1 needs to have addrinfo.AddrInfo stored in the endpoint - addr := multiaddr.StringCast("/ip4/" + fmt.Sprint(i+2) + "." + - fmt.Sprint(i+2) + "." + fmt.Sprint(i+2) + "." + fmt.Sprint(i+2)) - addrInfo := libp2p.NewAddrInfo(peer.AddrInfo{ - ID: p, - Addrs: []multiaddr.Multiaddr{addr}, - }) - // add peers to routing table and peerstore - err = invalidEP.MaybeAddToPeerstore(ctx, addrInfo, peerstoreTTL) - require.NoError(t, err) - success := rt.AddNode(peerids[i]) - require.True(t, success) - } - - s0 := NewBasicServer[multiaddr.Multiaddr](rt, invalidEP, WithPeerstoreTTL(peerstoreTTL), - WithNumberUsefulCloserPeers(numberOfCloserPeersToSend)) - - requesterPid, err := peer.Decode("1WoooREQUESTER") - require.NoError(t, err) - requester := libp2p.NewPeerID(requesterPid) - - // request will fail as endpoint is not a networked endpoint - req0 := libp2p.FindPeerRequest(self) - msg, err := s0.HandleRequest(ctx, requester, req0) - - require.Nil(t, msg) - require.Error(t, err) - require.Equal(t, ErrNotNetworkedEndpoint, err) - - // replace the key with an invalid peerid - req0.Key = []byte("invalid key") - msg, err = s0.HandleRequest(ctx, requester, req0) - - require.Nil(t, msg) - require.Error(t, err) - require.Equal(t, ErrIpfsV1InvalidPeerID, err) - - req0.Type = -1 // invalid request type - msg, err = s0.HandleRequest(ctx, requester, req0) - - require.Nil(t, msg) - require.Error(t, err) - require.Equal(t, ErrIpfsV1InvalidRequest, err) - - req1 := struct{}{} // invalid message format - msg, err = s0.HandleRequest(ctx, requester, req1) - - require.Nil(t, msg) - require.Error(t, err) - require.Equal(t, ErrUnknownMessageFormat, err) -} - -func TestIPFSv1Handling(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - peerstoreTTL := time.Second // doesn't matter as we use fakeendpoint - numberOfCloserPeersToSend := 4 - - selfPid, err := peer.Decode("1EooooSELF") - require.NoError(t, err) - self := libp2p.NewPeerID(selfPid) - - router := sim.NewRouter[key.Key256, multiaddr.Multiaddr]() - sched := event.NewSimpleScheduler(clk) - fakeEndpoint := sim.NewEndpoint[key.Key256, multiaddr.Multiaddr](self.NodeID(), sched, router) - rt := simplert.New[key.Key256, kad.NodeID[key.Key256]](self, 4) - - nPeers := 6 - peerids := make([]kad.NodeID[key.Key256], nPeers) - - for i := 0; i < nPeers; i++ { - // create peer.ID "1EoooPEER2" until "1EoooPEER7" - p, err := peer.Decode("1EoooPEER" + fmt.Sprint(i+2)) - require.NoError(t, err) - peerids[i] = libp2p.NewPeerID(p) - - // ipfsv1 needs to have addrinfo.AddrInfo stored in the endpoint - addr := multiaddr.StringCast("/ip4/" + fmt.Sprint(i+2) + "." + - fmt.Sprint(i+2) + "." + fmt.Sprint(i+2) + "." + fmt.Sprint(i+2)) - addrInfo := libp2p.NewAddrInfo(peer.AddrInfo{ - ID: p, - Addrs: []multiaddr.Multiaddr{addr}, - }) - if i == 1 { - // no addresses for peer 1, it should not be returned even though - // it is among the numberOfCloserPeersToSend closer peers - addrInfo = libp2p.NewAddrInfo(peer.AddrInfo{ - ID: p, - Addrs: nil, - }) - } - // add peers to routing table and peerstore - err = fakeEndpoint.MaybeAddToPeerstore(ctx, addrInfo, peerstoreTTL) - require.NoError(t, err) - success := rt.AddNode(peerids[i]) - require.True(t, success) - } - - // self: f3a2eb191b47b031e317e39776d09938c6d97a50b52e71acc4c4173275b998bd - // peerids[0]: e69614c5fcb92e8fbf2aa5785904fec5a67524ac7cd513f32bc7ab38621b4b7b (bucket 3) - // peerids[1]: 6ab9cb73bbd52ad2bb6ac4048e988478bf076df9b39e072f30b4722639382683 (bucket 0) - // peerids[2]: 69b9104f74ca05073a1bb658155fa4549fcc8db470947915a6e2750185dc1f81 (bucket 0) - // peerids[3]: 4eaafc67b177fa53ee6de27d1646f7862fb2957878bcc8d60dfa67b7832bb28b (bucket 0) - // peerids[4]: ab6c9fe862d32ff3170ed43600742b2abbb52f09216afa139cb89842e083ce4e (bucket 1) - // peerids[5]: 00ca8d64555add66790c4fb3e62075911a02a3577622fa69279731e82c135b8a (bucket 0) - - s0 := NewBasicServer[multiaddr.Multiaddr](rt, fakeEndpoint, WithPeerstoreTTL(peerstoreTTL), - WithNumberUsefulCloserPeers(numberOfCloserPeersToSend)) - - requesterPid, err := peer.Decode("1WoooREQUESTER") - require.NoError(t, err) - requester := libp2p.NewPeerID(requesterPid) - fakeEndpoint.MaybeAddToPeerstore(ctx, libp2p.NewAddrInfo(peer.AddrInfo{ - ID: requesterPid, - Addrs: nil, - }), peerstoreTTL) - - req0 := libp2p.FindPeerRequest(self) - msg, err := s0.HandleRequest(ctx, requester, req0) - require.NoError(t, err) - - resp, ok := msg.(*libp2p.Message) - require.True(t, ok) - - require.Len(t, resp.CloserNodes(), numberOfCloserPeersToSend-1) - // -1 because peerids[1] has no addresses, so it is counted as one of the - // numberOfCloserPeersToSend closer peers, but it is not returned - - order := []kad.NodeID[key.Key256]{peerids[0], peerids[4], peerids[2]} - for i, p := range resp.CloserNodes() { - require.Equal(t, order[i], p.ID()) - } -} diff --git a/server/server.go b/server/server.go deleted file mode 100644 index e94f189..0000000 --- a/server/server.go +++ /dev/null @@ -1,13 +0,0 @@ -package server - -import ( - "context" - - "github.com/plprobelab/go-kademlia/kad" -) - -// Server is the interface for handling requests from remote nodes. -type Server[K kad.Key[K]] interface { - // HandleRequest handles a request from a remote peer. - HandleRequest(context.Context, kad.NodeID[K], kad.Message) (kad.Message, error) -} diff --git a/sim/doc.go b/sim/doc.go deleted file mode 100644 index dbdd83f..0000000 --- a/sim/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package sim provides simple implementations of the principal Kademlia interfaces that allow the -// construction of simulations and test scenarios. The implementations are not suitable for use -// with real Kademlia networks. -package sim diff --git a/sim/endpoint.go b/sim/endpoint.go deleted file mode 100644 index cf7ad99..0000000 --- a/sim/endpoint.go +++ /dev/null @@ -1,255 +0,0 @@ -package sim - -import ( - "context" - "fmt" - "net" - "sync" - "time" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" - "github.com/plprobelab/go-kademlia/util" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// SimEndpoint is a simulated endpoint that doesn't operate on real network -type SimEndpoint[K kad.Key[K], A kad.Address[A]] interface { - endpoint.ServerEndpoint[K, A] - // HandleMessage handles a message from the given peer. - HandleMessage(context.Context, kad.NodeID[K], address.ProtocolID, - endpoint.StreamID, kad.Message) -} - -// Endpoint is a single threaded endpoint implementation simulating a network. -// It simulates a network and handles message exchanges between multiple peers in a simulation. -type Endpoint[K kad.Key[K], A kad.Address[A]] struct { - self kad.NodeID[K] - sched event.Scheduler // client - - peerstore map[string]kad.NodeInfo[K, A] - connStatus map[string]endpoint.Connectedness - serverProtos map[address.ProtocolID]endpoint.RequestHandlerFn[K] // server - - streamMu sync.Mutex // guards access to streamFollowup and streamTimeout - streamFollowup map[endpoint.StreamID]endpoint.ResponseHandlerFn[K, A] // client - streamTimeout map[endpoint.StreamID]event.PlannedAction // client - - router *Router[K, A] -} - -var _ SimEndpoint[key.Key256, net.IP] = (*Endpoint[key.Key256, net.IP])(nil) - -func NewEndpoint[K kad.Key[K], A kad.Address[A]](self kad.NodeID[K], sched event.Scheduler, router *Router[K, A]) *Endpoint[K, A] { - e := &Endpoint[K, A]{ - self: self, - sched: sched, - serverProtos: make(map[address.ProtocolID]endpoint.RequestHandlerFn[K]), - - peerstore: make(map[string]kad.NodeInfo[K, A]), - connStatus: make(map[string]endpoint.Connectedness), - - streamFollowup: make(map[endpoint.StreamID]endpoint.ResponseHandlerFn[K, A]), - streamTimeout: make(map[endpoint.StreamID]event.PlannedAction), - - router: router, - } - if router != nil { - router.AddPeer(self, e, sched) - } - return e -} - -func (e *Endpoint[K, A]) DialPeer(ctx context.Context, id kad.NodeID[K]) error { - _, span := util.StartSpan(ctx, "DialPeer", - trace.WithAttributes(attribute.String("id", id.String())), - ) - defer span.End() - - status, ok := e.connStatus[id.String()] - - if ok { - switch status { - case endpoint.Connected: - return nil - case endpoint.CanConnect: - e.connStatus[id.String()] = endpoint.Connected - return nil - } - } - span.RecordError(endpoint.ErrUnknownPeer) - return endpoint.ErrUnknownPeer -} - -// MaybeAddToPeerstore adds the given address to the peerstore. Endpoint -// doesn't take into account the ttl. -func (e *Endpoint[K, A]) MaybeAddToPeerstore(ctx context.Context, id kad.NodeInfo[K, A], ttl time.Duration) error { - strNodeID := id.ID().String() - _, span := util.StartSpan(ctx, "MaybeAddToPeerstore", - trace.WithAttributes(attribute.String("self", e.self.String())), - trace.WithAttributes(attribute.String("id", strNodeID)), - ) - defer span.End() - - if _, ok := e.peerstore[strNodeID]; !ok { - e.peerstore[strNodeID] = id - } - if _, ok := e.connStatus[strNodeID]; !ok { - e.connStatus[strNodeID] = endpoint.CanConnect - } - return nil -} - -func (e *Endpoint[K, A]) SendRequestHandleResponse(ctx context.Context, - protoID address.ProtocolID, id kad.NodeID[K], req kad.Message, - resp kad.Message, timeout time.Duration, - handleResp endpoint.ResponseHandlerFn[K, A], -) error { - ctx, span := util.StartSpan(ctx, "SendRequestHandleResponse", - trace.WithAttributes(attribute.Stringer("id", id)), - ) - defer span.End() - - if err := e.DialPeer(ctx, id); err != nil { - span.RecordError(err) - e.sched.EnqueueAction(ctx, event.BasicAction(func(ctx context.Context) { - handleResp(ctx, nil, err) - })) - return nil - } - - // send request. id.String() is guaranteed to be in peerstore, because - // DialPeer checks it, and an error is returned if it's not there. - addr := e.peerstore[id.String()] - - sid, err := e.router.SendMessage(ctx, e.self, addr.ID(), protoID, 0, req) - if err != nil { - span.RecordError(err) - e.sched.EnqueueAction(ctx, event.BasicAction(func(ctx context.Context) { - handleResp(ctx, nil, err) - })) - return nil - } - e.streamMu.Lock() - defer e.streamMu.Unlock() - - e.streamFollowup[sid] = handleResp - // timeout - if timeout != 0 { - e.streamTimeout[sid] = event.ScheduleActionIn(ctx, e.sched, timeout, - event.BasicAction(func(ctx context.Context) { - ctx, span := util.StartSpan(ctx, "SendRequestHandleResponse timeout", - trace.WithAttributes(attribute.Stringer("id", id)), - ) - defer span.End() - - e.streamMu.Lock() - handleFn, ok := e.streamFollowup[sid] - delete(e.streamFollowup, sid) - delete(e.streamTimeout, sid) - e.streamMu.Unlock() - - if !ok || handleFn == nil { - span.RecordError(fmt.Errorf("no followup for stream %d", sid)) - return - } - handleFn(ctx, nil, endpoint.ErrTimeout) - })) - } - return nil -} - -// Peerstore functions -func (e *Endpoint[K, A]) Connectedness(id kad.NodeID[K]) (endpoint.Connectedness, error) { - if s, ok := e.connStatus[id.String()]; !ok { - return endpoint.NotConnected, nil - } else { - return s, nil - } -} - -func (e *Endpoint[K, A]) NetworkAddress(id kad.NodeID[K]) (kad.NodeInfo[K, A], error) { - if ai, ok := e.peerstore[id.String()]; ok { - return ai, nil - } - if na, ok := id.(kad.NodeInfo[K, A]); ok { - return na, nil - } - return nil, endpoint.ErrUnknownPeer -} - -func (e *Endpoint[K, A]) Key() K { - return e.self.Key() -} - -func (e *Endpoint[K, A]) HandleMessage(ctx context.Context, id kad.NodeID[K], - protoID address.ProtocolID, sid endpoint.StreamID, msg kad.Message, -) { - _, span := util.StartSpan(ctx, "HandleMessage", - trace.WithAttributes(attribute.Stringer("id", id), - attribute.Int64("StreamID", int64(sid)))) - defer span.End() - - e.streamMu.Lock() - followup, ok := e.streamFollowup[sid] - e.streamMu.Unlock() - - if ok { - span.AddEvent("Response to previous request") - - e.streamMu.Lock() - timeout, ok := e.streamTimeout[sid] - if ok { - e.sched.RemovePlannedAction(ctx, timeout) - } - // remove stream id from endpoint - delete(e.streamFollowup, sid) - delete(e.streamTimeout, sid) - e.streamMu.Unlock() - - resp, ok := msg.(kad.Response[K, A]) - var err error - if ok { - for _, p := range resp.CloserNodes() { - e.peerstore[p.ID().String()] = p - e.connStatus[p.ID().String()] = endpoint.CanConnect - } - } else { - err = ErrInvalidResponseType - } - if followup != nil { - e.sched.EnqueueAction(ctx, event.BasicAction(func(ctx context.Context) { - followup(ctx, resp, err) - })) - } - return - } - - if handler, ok := e.serverProtos[protoID]; ok && handler != nil { - // it isn't a response, so treat it as a request - resp, err := handler(ctx, id, msg) - if err != nil { - span.RecordError(err) - return - } - e.router.SendMessage(ctx, e.self, id, protoID, sid, resp) - } -} - -func (e *Endpoint[K, A]) AddRequestHandler(protoID address.ProtocolID, - req kad.Message, reqHandler endpoint.RequestHandlerFn[K], -) error { - if reqHandler == nil { - return endpoint.ErrNilRequestHandler - } - e.serverProtos[protoID] = reqHandler - return nil -} - -func (e *Endpoint[K, A]) RemoveRequestHandler(protoID address.ProtocolID) { - delete(e.serverProtos, protoID) -} diff --git a/sim/endpoint_test.go b/sim/endpoint_test.go deleted file mode 100644 index a8c6cdd..0000000 --- a/sim/endpoint_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package sim - -import ( - "context" - "net" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" - "github.com/plprobelab/go-kademlia/routing/simplert" -) - -var ( - _ endpoint.NetworkedEndpoint[key.Key8, net.IP] = (*Endpoint[key.Key8, net.IP])(nil) - _ SimEndpoint[key.Key8, net.IP] = (*Endpoint[key.Key8, net.IP])(nil) - _ endpoint.Endpoint[key.Key8, net.IP] = (*Endpoint[key.Key8, net.IP])(nil) -) - -var ( - protoID = address.ProtocolID("/test/1.0.0") - peerstoreTTL = time.Minute -) - -func TestEndpoint(t *testing.T) { - t.Skip() - - ctx := context.Background() - clk := clock.NewMock() - - selfID := kadtest.StringID("self") - - router := NewRouter[key.Key256, net.IP]() - sched := event.NewSimpleScheduler(clk) - - fakeEndpoint := NewEndpoint[key.Key256, net.IP](selfID.NodeID(), sched, router) - - b := key.Equal(selfID.Key(), fakeEndpoint.Key()) - require.True(t, b) - - node0 := kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.StringID("node0").Key()), nil) - err := fakeEndpoint.DialPeer(ctx, node0.ID()) - require.Equal(t, endpoint.ErrUnknownPeer, err) - - connectedness, err := fakeEndpoint.Connectedness(node0.ID()) - require.NoError(t, err) - require.Equal(t, endpoint.NotConnected, connectedness) - - na, err := fakeEndpoint.NetworkAddress(node0.ID()) - require.Error(t, err) - require.Nil(t, na) - - pid := kadtest.NewStringID("some-id") - _, err = fakeEndpoint.NetworkAddress(pid) - require.Equal(t, endpoint.ErrUnknownPeer, err) - - req := NewRequest[key.Key256, net.IP](selfID.Key()) - resp := &Message[key.Key256, net.IP]{} - - var runCheck bool - respHandler := func(ctx context.Context, msg kad.Response[key.Key256, net.IP], err error) { - require.Equal(t, endpoint.ErrUnknownPeer, err) - runCheck = true - } - fakeEndpoint.SendRequestHandleResponse(ctx, protoID, node0.ID(), req, resp, 0, respHandler) - require.True(t, sched.RunOne(ctx)) - require.False(t, sched.RunOne(ctx)) - require.True(t, runCheck) - - err = fakeEndpoint.MaybeAddToPeerstore(ctx, kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(node0.ID().Key()), nil), peerstoreTTL) - require.NoError(t, err) - - connectedness, err = fakeEndpoint.Connectedness(node0.ID()) - require.NoError(t, err) - require.Equal(t, endpoint.CanConnect, connectedness) - - na, err = fakeEndpoint.NetworkAddress(node0.ID()) - require.NoError(t, err) - require.Equal(t, node0.ID(), na) - - // it will still be an ErrUnknownPeer because we haven't added node0.ID() to the router - err = fakeEndpoint.SendRequestHandleResponse(ctx, protoID, node0.ID(), req, resp, 0, respHandler) - require.NoError(t, err) - require.True(t, sched.RunOne(ctx)) - require.False(t, sched.RunOne(ctx)) - - sched0 := event.NewSimpleScheduler(clk) - fakeEndpoint0 := NewEndpoint[key.Key256, net.IP](node0.ID(), sched0, router) - rt0 := simplert.New[key.Key256, kad.NodeID[key.Key256]](node0.ID(), 2) - serv0 := NewServer[key.Key256, net.IP](rt0, fakeEndpoint0, DefaultServerConfig()) - err = fakeEndpoint0.AddRequestHandler(protoID, nil, serv0.HandleRequest) - require.NoError(t, err) - err = fakeEndpoint0.AddRequestHandler(protoID, nil, nil) - require.Equal(t, endpoint.ErrNilRequestHandler, err) - // remove a request handler that doesn't exist - fakeEndpoint0.RemoveRequestHandler("/test/0.0.1") - - runCheck = false - respHandler = func(ctx context.Context, msg kad.Response[key.Key256, net.IP], err error) { - require.NoError(t, err) - runCheck = true - } - - err = fakeEndpoint.SendRequestHandleResponse(ctx, protoID, node0.ID(), req, resp, 0, respHandler) - require.NoError(t, err) - - require.True(t, sched0.RunOne(ctx)) - require.False(t, sched0.RunOne(ctx)) - - require.False(t, runCheck) - - require.True(t, sched.RunOne(ctx)) - require.True(t, sched.RunOne(ctx)) - require.False(t, sched.RunOne(ctx)) - - require.True(t, runCheck) - - // test response to a sent request that is not a valid Response - var sid endpoint.StreamID = 1000 - fakeEndpoint.streamFollowup[sid] = func(ctx context.Context, - msg kad.Response[key.Key256, net.IP], err error, - ) { - require.Equal(t, ErrInvalidResponseType, err) - } - var msg kad.Message - fakeEndpoint.HandleMessage(ctx, node0.ID(), protoID, sid, msg) - - require.True(t, sched.RunOne(ctx)) - require.False(t, sched.RunOne(ctx)) - - // test request whose handler returns an error - errHandler := func(ctx context.Context, id kad.NodeID[key.Key256], - req kad.Message, - ) (kad.Message, error) { - return nil, endpoint.ErrUnknownPeer - } - errProtoID := address.ProtocolID("/err/0.0.1") - fakeEndpoint.AddRequestHandler(errProtoID, nil, errHandler) - fakeEndpoint.HandleMessage(ctx, node0.ID(), errProtoID, 1001, msg) - // no message should have been sent to node0.ID(), so nothing to run - require.False(t, sched0.RunOne(ctx)) - - var followupRan bool - // test that HandleMessage adds the closer peers to the peerstore - followup := func(ctx context.Context, resp kad.Response[key.Key256, net.IP], - err error, - ) { - require.NoError(t, err) - followupRan = true - } - // add followup function for the stream and make sure it runs - fakeEndpoint.streamFollowup[1000] = followup - addrs := []kad.NodeInfo[key.Key256, net.IP]{ - kadtest.NewInfo(kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{0})), []net.IP{net.ParseIP("127.0.0.1")}), - kadtest.NewInfo(kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{1})), []net.IP{net.ParseIP("127.0.0.2")}), - kadtest.NewInfo(kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{2})), []net.IP{net.ParseIP("127.0.0.3")}), - } - msg = NewResponse(addrs) - fakeEndpoint.HandleMessage(ctx, node0.ID(), protoID, 1000, msg) - - a, err := fakeEndpoint.NetworkAddress(addrs[0].ID()) - require.NoError(t, err) - require.Equal(t, addrs[0], a) - - require.True(t, sched.RunOne(ctx)) - require.False(t, sched.RunOne(ctx)) - require.True(t, followupRan) -} - -func TestRequestTimeout(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - router := NewRouter[key.Key256, net.IP]() - - nPeers := 2 - scheds := make([]event.AwareScheduler, nPeers) - ids := make([]kad.NodeInfo[key.Key256, net.IP], nPeers) - fakeEndpoints := make([]*Endpoint[key.Key256, net.IP], nPeers) - for i := 0; i < nPeers; i++ { - ids[i] = kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{byte(i)})), nil) - scheds[i] = event.NewSimpleScheduler(clk) - fakeEndpoints[i] = NewEndpoint[key.Key256, net.IP](ids[i].ID(), scheds[i], router) - } - - // connect the peers to each other - for i, fe := range fakeEndpoints { - for j := range fakeEndpoints { - if i != j { - fe.MaybeAddToPeerstore(ctx, ids[j], peerstoreTTL) - } - } - } - - var timeoutExecuted bool - // fakeEndpoints[1]'s request handler will not respond - dropRequestHandler := func(ctx context.Context, id kad.NodeID[key.Key256], - req kad.Message, - ) (kad.Message, error) { - return nil, endpoint.ErrUnknownPeer - } - fakeEndpoints[1].AddRequestHandler(protoID, nil, dropRequestHandler) - // fakeEndpoints[0] will send a request to fakeEndpoints[1], but the request - // will timeout (because fakeEndpoints[1] will not respond) - fakeEndpoints[0].SendRequestHandleResponse(ctx, protoID, ids[1].ID(), nil, nil, - time.Second, func(ctx context.Context, - msg kad.Response[key.Key256, net.IP], err error, - ) { - timeoutExecuted = true - }) - - // peer[1] has 1 action to run: message handling and not responding - require.True(t, scheds[1].RunOne(ctx)) - require.False(t, scheds[1].RunOne(ctx)) - // peer[0] has no action to run: waiting for response - require.False(t, scheds[0].RunOne(ctx)) - - // advance the clock to timeout - clk.Add(time.Second) - - // peer[0] is running timeout action - require.True(t, scheds[0].RunOne(ctx)) - require.False(t, scheds[0].RunOne(ctx)) - require.True(t, timeoutExecuted) - - // timeout without followup action - fakeEndpoints[0].SendRequestHandleResponse(ctx, protoID, ids[1].ID(), nil, nil, - time.Second, nil) - // peer[1] has 1 action to run: message handling and not responding - require.True(t, scheds[1].RunOne(ctx)) - require.False(t, scheds[1].RunOne(ctx)) - // peer[0] has no action to run: waiting for response - require.False(t, scheds[0].RunOne(ctx)) - // advance the clock to timeout - clk.Add(time.Second) - // peer[0] is running timeout action - require.True(t, scheds[0].RunOne(ctx)) - require.False(t, scheds[0].RunOne(ctx)) - - // response coming back before timeout - var handlerHasResponded bool - dumbResponseHandler := func(ctx context.Context, id kad.NodeID[key.Key256], - req kad.Message, - ) (kad.Message, error) { - clk.Sleep(100 * time.Millisecond) - return req, nil - } - // create valid message - msg := NewResponse([]kad.NodeInfo[key.Key256, net.IP]{}) - // overwrite request handler - fakeEndpoints[1].AddRequestHandler(protoID, nil, dumbResponseHandler) - fakeEndpoints[0].SendRequestHandleResponse(ctx, protoID, ids[1].ID(), msg, nil, - time.Second, func(ctx context.Context, - msg kad.Response[key.Key256, net.IP], err error, - ) { - require.NoError(t, err) - }) - - go func() { - for !handlerHasResponded { - clk.Add(100 * time.Millisecond) - } - }() - // peer[0] is waiting for response - require.False(t, scheds[0].RunOne(ctx)) - // peer[1] has 1 action to run: message handling and responding - require.True(t, scheds[1].RunOne(ctx)) - require.False(t, scheds[1].RunOne(ctx)) - // peer[0] is running followup action - require.True(t, scheds[0].RunOne(ctx)) - require.True(t, scheds[0].RunOne(ctx)) - require.False(t, scheds[0].RunOne(ctx)) -} diff --git a/sim/errors.go b/sim/errors.go deleted file mode 100644 index c35522f..0000000 --- a/sim/errors.go +++ /dev/null @@ -1,9 +0,0 @@ -package sim - -import "errors" - -var ( - ErrNotNetworkedEndpoint = errors.New("endpoint is not a NetworkedEndpoint") - ErrUnknownMessageFormat = errors.New("unknown message format") - ErrInvalidResponseType = errors.New("invalid response type, expected MinKadResponseMessage") -) diff --git a/sim/message.go b/sim/message.go deleted file mode 100644 index af8d223..0000000 --- a/sim/message.go +++ /dev/null @@ -1,36 +0,0 @@ -package sim - -import ( - "github.com/plprobelab/go-kademlia/kad" -) - -// Message is a simple implementation of `Request` and `Response`. -// It only contains the minimal fields that are required by Kademlia to operate. -type Message[K kad.Key[K], A kad.Address[A]] struct { - target K - closerPeers []kad.NodeInfo[K, A] -} - -func NewRequest[K kad.Key[K], A kad.Address[A]](target K) *Message[K, A] { - return &Message[K, A]{ - target: target, - } -} - -func NewResponse[K kad.Key[K], A kad.Address[A]](closerPeers []kad.NodeInfo[K, A]) *Message[K, A] { - return &Message[K, A]{ - closerPeers: closerPeers, - } -} - -func (m *Message[K, A]) Target() K { - return m.target -} - -func (m *Message[K, A]) EmptyResponse() kad.Response[K, A] { - return &Message[K, A]{} -} - -func (m *Message[K, A]) CloserNodes() []kad.NodeInfo[K, A] { - return m.closerPeers -} diff --git a/sim/message_test.go b/sim/message_test.go deleted file mode 100644 index de811c2..0000000 --- a/sim/message_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package sim - -import ( - "net" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" -) - -var ( - _ kad.Request[key.Key8, net.IP] = (*Message[key.Key8, net.IP])(nil) - _ kad.Response[key.Key8, net.IP] = (*Message[key.Key8, net.IP])(nil) -) - -func TestRequest(t *testing.T) { - target := kadtest.StringID("target") - msg := NewRequest[key.Key256, net.IP](target.Key()) - - require.Equal(t, &Message[key.Key256, net.IP]{}, msg.EmptyResponse()) - - b := key.Equal(msg.Target(), target.Key()) - require.True(t, b) - require.Nil(t, msg.CloserNodes()) -} - -func TestResponse(t *testing.T) { - closerPeers := []kad.NodeInfo[key.Key256, net.IP]{ - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.StringID("peer1").Key()), nil), - kadtest.NewInfo[key.Key256, net.IP](kadtest.NewID(kadtest.StringID("peer2").Key()), nil), - } - msg := NewResponse(closerPeers) - - // require.Nil(t, msg.Target()) - require.Equal(t, len(closerPeers), len(msg.CloserNodes())) - for i, peer := range closerPeers { - require.Equal(t, closerPeers[i], peer) - } -} diff --git a/sim/router.go b/sim/router.go deleted file mode 100644 index 1cfd737..0000000 --- a/sim/router.go +++ /dev/null @@ -1,51 +0,0 @@ -package sim - -import ( - "context" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" -) - -type Router[K kad.Key[K], A kad.Address[A]] struct { - currStream endpoint.StreamID - peers map[string]SimEndpoint[K, A] - scheds map[string]event.Scheduler -} - -func NewRouter[K kad.Key[K], A kad.Address[A]]() *Router[K, A] { - return &Router[K, A]{ - currStream: 1, - peers: make(map[string]SimEndpoint[K, A]), - scheds: make(map[string]event.Scheduler), - } -} - -func (r *Router[K, A]) AddPeer(id kad.NodeID[K], peer SimEndpoint[K, A], sched event.Scheduler) { - r.peers[id.String()] = peer - r.scheds[id.String()] = sched -} - -func (r *Router[K, A]) RemovePeer(id kad.NodeID[K]) { - delete(r.peers, id.String()) - delete(r.scheds, id.String()) -} - -func (r *Router[K, A]) SendMessage(ctx context.Context, from, to kad.NodeID[K], - protoID address.ProtocolID, sid endpoint.StreamID, - msg kad.Message, -) (endpoint.StreamID, error) { - if _, ok := r.peers[to.String()]; !ok { - return 0, endpoint.ErrUnknownPeer - } - if sid == 0 { - sid = r.currStream - r.currStream++ - } - r.scheds[to.String()].EnqueueAction(ctx, event.BasicAction(func(ctx context.Context) { - r.peers[to.String()].HandleMessage(ctx, from, protoID, sid, msg) - })) - return sid, nil -} diff --git a/sim/router_test.go b/sim/router_test.go deleted file mode 100644 index f67d2d3..0000000 --- a/sim/router_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package sim - -import ( - "context" - "net" - "testing" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/address" - "github.com/plprobelab/go-kademlia/network/endpoint" -) - -func TestRouter(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - router := NewRouter[key.Key256, net.IP]() - - nPeers := 5 - scheds := make([]event.AwareScheduler, nPeers) - ids := make([]kad.NodeID[key.Key256], nPeers) - fakeEndpoints := make([]*Endpoint[key.Key256, net.IP], nPeers) - for i := 0; i < nPeers; i++ { - ids[i] = kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{byte(i)})) - scheds[i] = event.NewSimpleScheduler(clk) - fakeEndpoints[i] = NewEndpoint[key.Key256, net.IP](ids[i], scheds[i], router) - } - - protoID := address.ProtocolID("/test/proto") - sid, err := router.SendMessage(ctx, ids[0], ids[1], protoID, 0, nil) - require.NoError(t, err) - require.Equal(t, endpoint.StreamID(1), sid) - require.True(t, scheds[1].RunOne(ctx)) - require.False(t, scheds[1].RunOne(ctx)) - - newSid := endpoint.StreamID(100) - sid, err = router.SendMessage(ctx, ids[4], ids[2], protoID, newSid, nil) - require.NoError(t, err) - require.Equal(t, newSid, sid) - require.True(t, scheds[2].RunOne(ctx)) - require.False(t, scheds[2].RunOne(ctx)) - - sid, err = router.SendMessage(ctx, ids[2], ids[3], protoID, 0, nil) - require.NoError(t, err) - require.Equal(t, endpoint.StreamID(2), sid) - require.True(t, scheds[3].RunOne(ctx)) - require.False(t, scheds[3].RunOne(ctx)) - - notRegisteredID := kadtest.NewID(kadtest.Key256WithLeadingBytes([]byte{byte(100)})) - sid, err = router.SendMessage(ctx, ids[3], notRegisteredID, protoID, 0, nil) - require.Error(t, err) - require.Equal(t, endpoint.ErrUnknownPeer, err) - require.Equal(t, endpoint.StreamID(0), sid) - - router.RemovePeer(ids[1]) - sid, err = router.SendMessage(ctx, ids[0], ids[1], protoID, 0, nil) - require.Error(t, err) - require.Equal(t, endpoint.ErrUnknownPeer, err) - require.Equal(t, endpoint.StreamID(0), sid) - require.False(t, scheds[1].RunOne(ctx)) -} diff --git a/sim/server.go b/sim/server.go deleted file mode 100644 index 8dbd1bc..0000000 --- a/sim/server.go +++ /dev/null @@ -1,100 +0,0 @@ -package sim - -import ( - "context" - "time" - - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/network/endpoint" - "github.com/plprobelab/go-kademlia/util" -) - -type Server[K kad.Key[K], A kad.Address[A]] struct { - rt kad.RoutingTable[K, kad.NodeID[K]] - endpoint endpoint.Endpoint[K, A] - - peerstoreTTL time.Duration - numberOfCloserPeersToSend int -} - -func NewServer[K kad.Key[K], A kad.Address[A]](rt kad.RoutingTable[K, kad.NodeID[K]], endpoint endpoint.Endpoint[K, A], cfg *ServerConfig) *Server[K, A] { - if cfg == nil { - cfg = DefaultServerConfig() - } - return &Server[K, A]{ - rt: rt, - endpoint: endpoint, - peerstoreTTL: cfg.PeerstoreTTL, - numberOfCloserPeersToSend: cfg.NumberUsefulCloserPeers, - } -} - -func (s *Server[K, A]) HandleRequest(ctx context.Context, rpeer kad.NodeID[K], - msg kad.Message, -) (kad.Message, error) { - switch msg := msg.(type) { - case *Message[K, A]: - return s.HandleFindNodeRequest(ctx, rpeer, msg) - default: - return nil, ErrUnknownMessageFormat - } -} - -func (s *Server[K, A]) HandleFindNodeRequest(ctx context.Context, - rpeer kad.NodeID[K], msg kad.Message, -) (kad.Message, error) { - var target K - - switch msg := msg.(type) { - case *Message[K, A]: - target = msg.Target() - default: - // invalid request, don't reply - return nil, ErrUnknownMessageFormat - } - - _, span := util.StartSpan(ctx, "Server.HandleFindNodeRequest", trace.WithAttributes( - attribute.Stringer("Requester", rpeer), - attribute.String("Target", key.HexString(target)))) - defer span.End() - - nodes := s.rt.NearestNodes(target, s.numberOfCloserPeersToSend) - span.AddEvent("Nearest nodes", trace.WithAttributes( - attribute.Int("count", len(nodes)), - )) - - var resp kad.Message - switch msg.(type) { - case *Message[K, A]: - peerAddrs := make([]kad.NodeInfo[K, A], len(nodes)) - var index int - for _, p := range nodes { - na, err := s.endpoint.NetworkAddress(p) - if err != nil { - span.RecordError(err) - continue - } - peerAddrs[index] = na - index++ - } - resp = NewResponse(peerAddrs[:index]) - } - - return resp, nil -} - -type ServerConfig struct { - PeerstoreTTL time.Duration - NumberUsefulCloserPeers int -} - -func DefaultServerConfig() *ServerConfig { - return &ServerConfig{ - PeerstoreTTL: time.Second, - NumberUsefulCloserPeers: 4, - } -} diff --git a/sim/server_test.go b/sim/server_test.go deleted file mode 100644 index 0d38fc4..0000000 --- a/sim/server_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package sim - -import ( - "context" - "fmt" - "net" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/stretchr/testify/require" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/internal/kadtest" - "github.com/plprobelab/go-kademlia/kad" - "github.com/plprobelab/go-kademlia/key" - "github.com/plprobelab/go-kademlia/routing/simplert" -) - -// remotePeers with bucket assignments wrt to self -var kadRemotePeers = []kad.NodeInfo[key.Key8, net.IP]{ - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b10001000)), nil), // 1000 1000 (bucket 0) - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b11010010)), nil), // 1101 0010 (bucket 0) - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b01001011)), nil), // 0100 1011 (bucket 1) - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b01010011)), nil), // 0101 0011 (bucket 1) - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b00101110)), nil), // 0010 1110 (bucket 2) - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b00110110)), nil), // 0011 0110 (bucket 2) - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b00011111)), nil), // 0001 1111 (bucket 3) - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b00010001)), nil), // 0001 0001 (bucket 3) - kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b00001000)), nil), // 0000 1000 (bucket 4) -} - -func TestMessageHandling(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - peerstoreTTL := time.Second - numberOfCloserPeersToSend := 4 - - self := kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0)), nil) // 0000 0000 - - router := NewRouter[key.Key8, net.IP]() - sched := event.NewSimpleScheduler(clk) - fakeEndpoint := NewEndpoint[key.Key8, net.IP](self.ID(), sched, router) - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](self.ID(), 2) - - // add peers to routing table and peerstore - for _, p := range kadRemotePeers { - err := fakeEndpoint.MaybeAddToPeerstore(ctx, p, peerstoreTTL) - require.NoError(t, err) - success := rt.AddNode(p.ID()) - require.True(t, success) - } - - s0 := NewServer[key.Key8, net.IP](rt, fakeEndpoint, &ServerConfig{ - PeerstoreTTL: peerstoreTTL, - NumberUsefulCloserPeers: numberOfCloserPeersToSend, - }) - - requester := kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0b00000001)), nil) // 0000 0001 - fakeEndpoint.MaybeAddToPeerstore(ctx, requester, peerstoreTTL) - - req0 := NewRequest[key.Key8, net.IP](key.Key8(0b00000000)) - msg, err := s0.HandleRequest(ctx, requester.ID(), req0) - require.NoError(t, err) - - resp, ok := msg.(kad.Response[key.Key8, net.IP]) - require.True(t, ok) - require.Len(t, resp.CloserNodes(), numberOfCloserPeersToSend) - // closer peers should be ordered by distance to 0000 0000 - // [8] 0000 1000, [7] 0001 0001, [6] 0001 1111, [4] 0010 1110 - order := []kad.NodeInfo[key.Key8, net.IP]{ - kadRemotePeers[8], kadRemotePeers[7], - kadRemotePeers[6], kadRemotePeers[4], - } - for i, p := range resp.CloserNodes() { - require.Equal(t, order[i], p) - } - - req1 := NewRequest[key.Key8, net.IP](key.Key8(0b11111111)) - msg, err = s0.HandleRequest(ctx, requester.ID(), req1) - require.NoError(t, err) - resp, ok = msg.(kad.Response[key.Key8, net.IP]) - require.True(t, ok) - require.Len(t, resp.CloserNodes(), numberOfCloserPeersToSend) - // closer peers should be ordered by distance to 1111 1111 - // [1] 1101 0010, [0] 1000 1000, [3] 0101 0011, [2] 0100 1011 - order = []kad.NodeInfo[key.Key8, net.IP]{ - kadRemotePeers[1], kadRemotePeers[0], - kadRemotePeers[3], kadRemotePeers[2], - } - for i, p := range resp.CloserNodes() { - require.Equal(t, order[i], p) - } - - numberOfCloserPeersToSend = 3 - s1 := NewServer[key.Key8, net.IP](rt, fakeEndpoint, &ServerConfig{ - PeerstoreTTL: peerstoreTTL, - NumberUsefulCloserPeers: numberOfCloserPeersToSend, - }) - - req2 := NewRequest[key.Key8, net.IP](key.Key8(0b01100000)) - msg, err = s1.HandleRequest(ctx, requester.ID(), req2) - require.NoError(t, err) - resp, ok = msg.(kad.Response[key.Key8, net.IP]) - require.True(t, ok) - require.Len(t, resp.CloserNodes(), numberOfCloserPeersToSend) - // closer peers should be ordered by distance to 0110 0000 - // [2] 0100 1011, [3] 0101 0011, [4] 0010 1110 - order = []kad.NodeInfo[key.Key8, net.IP]{kadRemotePeers[2], kadRemotePeers[3], kadRemotePeers[4]} - for i, p := range resp.CloserNodes() { - require.Equal(t, order[i], p) - } -} - -func TestInvalidSimRequests(t *testing.T) { - ctx := context.Background() - // invalid option - s := (*Server[key.Key8, net.IP])(nil) - require.Nil(t, s) - - peerstoreTTL := time.Second // doesn't matter as we use fakeendpoint - - clk := clock.New() - router := NewRouter[key.Key8, net.IP]() - - self := kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0)), nil) // 0000 0000 - - // create a valid server - sched := event.NewSimpleScheduler(clk) - fakeEndpoint := NewEndpoint[key.Key8, net.IP](self.ID(), sched, router) - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](self.ID(), 2) - - // add peers to routing table and peerstore - for _, p := range kadRemotePeers { - err := fakeEndpoint.MaybeAddToPeerstore(ctx, p, peerstoreTTL) - require.NoError(t, err) - success := rt.AddNode(p.ID()) - require.NoError(t, err) - require.True(t, success) - } - - s = NewServer[key.Key8, net.IP](rt, fakeEndpoint, DefaultServerConfig()) - require.NotNil(t, s) - - requester := kadtest.NewID(key.Key8(0b00000001)) // 0000 0001 - - // invalid message format (not a Message) - req0 := struct{}{} - _, err := s.HandleFindNodeRequest(ctx, requester, req0) - require.Error(t, err) - - // empty request - req1 := &Message[key.Key8, net.IP]{} - s.HandleFindNodeRequest(ctx, requester, req1) - - // request with invalid key (not matching the expected length) - req2 := NewRequest[key.Key32, net.IP](key.Key32(0b00000000000000010000000000000000)) - s.HandleFindNodeRequest(ctx, requester, req2) -} - -func TestRequestNoNetworkAddress(t *testing.T) { - ctx := context.Background() - - clk := clock.New() - router := NewRouter[key.Key8, net.IP]() - - self := kadtest.NewInfo[key.Key8, net.IP](kadtest.NewID(key.Key8(0)), nil) // 0000 0000 - - // create a valid server - sched := event.NewSimpleScheduler(clk) - fakeEndpoint := NewEndpoint[key.Key8, net.IP](self.ID(), sched, router) - rt := simplert.New[key.Key8, kad.NodeID[key.Key8]](self.ID(), 2) - - node := kadtest.NewID(key.Key8(0xf6)) - - // add peer to routing table, but NOT to peerstore - success := rt.AddNode(node) - require.True(t, success) - - s := NewServer[key.Key8, net.IP](rt, fakeEndpoint, DefaultServerConfig()) - require.NotNil(t, s) - - requester := kadtest.NewID(key.Key8(0x80)) - - // sim request message (for net.IP key) - req := NewRequest[key.Key8, net.IP](requester.Key()) - msg, err := s.HandleFindNodeRequest(ctx, requester, req) - require.NoError(t, err) - resp, ok := msg.(kad.Response[key.Key8, net.IP]) - require.True(t, ok) - fmt.Println(resp.CloserNodes()) - require.Len(t, resp.CloserNodes(), 0) -} diff --git a/sim/simulator.go b/sim/simulator.go deleted file mode 100644 index 1d89dca..0000000 --- a/sim/simulator.go +++ /dev/null @@ -1,122 +0,0 @@ -package sim - -import ( - "context" - "time" - - "github.com/benbjohnson/clock" - - "github.com/plprobelab/go-kademlia/event" - "github.com/plprobelab/go-kademlia/util" -) - -// Simulator is an interface for simulating a set of schedulers. -type Simulator interface { - // Add adds a scheduler to the simulator - Add(event.AwareScheduler) - // Remove removes a scheduler from the simulator - Remove(event.AwareScheduler) - // Run runs the simulator until there are no more Actions to run - Run(context.Context) -} - -// AddSchedulers adds a set of schedulers to a simulator -func AddSchedulers(s Simulator, schedulers ...event.AwareScheduler) { - for _, sched := range schedulers { - s.Add(sched) - } -} - -// RemoveSchedulers removes a set of schedulers from a simulator -func RemoveSchedulers(s Simulator, schedulers ...event.AwareScheduler) { - for _, sched := range schedulers { - s.Remove(sched) - } -} - -type LiteSimulator struct { - clk *clock.Mock - schedulers []event.AwareScheduler // replace with custom linked list -} - -var _ Simulator = (*LiteSimulator)(nil) - -func NewLiteSimulator(clk *clock.Mock) *LiteSimulator { - return &LiteSimulator{ - clk: clk, - schedulers: make([]event.AwareScheduler, 0), - } -} - -func (s *LiteSimulator) Clock() *clock.Mock { - return s.clk -} - -func (s *LiteSimulator) Add(sched event.AwareScheduler) { - s.schedulers = append(s.schedulers, sched) -} - -func (s *LiteSimulator) Remove(sched event.AwareScheduler) { - for i, sch := range s.schedulers { - if sch == sched { - s.schedulers = append(s.schedulers[:i], s.schedulers[i+1:]...) - } - } -} - -func (s *LiteSimulator) Run(ctx context.Context) { - ctx, span := util.StartSpan(ctx, "SimpleDispatcher.DispatchLoop") - defer span.End() - - // get the next action time for each peer - nextActions := make([]time.Time, len(s.schedulers)) - for i, sched := range s.schedulers { - nextActions[i] = sched.NextActionTime(ctx) - } - - for len(nextActions) > 0 { - // find the time of the next action to be run - minTime := event.MaxTime - for _, t := range nextActions { - if t.Before(minTime) { - minTime = t - } - } - - if minTime == event.MaxTime { - // no more actions to run - break - } - - upNext := make([]int, 0) - for id, t := range nextActions { - if t == minTime { - upNext = append(upNext, id) - } - } - - if minTime.After(s.clk.Now()) { - // "wait" minTime for the next action - s.clk.Set(minTime) // slow to execute (because of the mutex?) - } - - for len(upNext) > 0 { - ongoing := make([]int, len(upNext)) - copy(ongoing, upNext) - - upNext = make([]int, 0) - for _, id := range ongoing { - // run one action for this peer - s.schedulers[id].RunOne(ctx) - } - for id, s := range s.schedulers { - t := s.NextActionTime(ctx) - if t == minTime { - upNext = append(upNext, id) - } else { - nextActions[id] = t - } - } - } - } -} diff --git a/sim/simulator_test.go b/sim/simulator_test.go deleted file mode 100644 index c6f259a..0000000 --- a/sim/simulator_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package sim - -import ( - "context" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/plprobelab/go-kademlia/event" - "github.com/stretchr/testify/require" -) - -func TestLiteSimulator(t *testing.T) { - ctx := context.Background() - clk := clock.NewMock() - - nNodes := 7 - scheds := make([]event.AwareScheduler, nNodes) - - for i := 0; i < nNodes; i++ { - scheds[i] = event.NewSimpleScheduler(clk) - } - - sim := NewLiteSimulator(clk) - AddSchedulers(sim, scheds...) - - RemoveSchedulers(sim, scheds[0], scheds[3], scheds[6]) - require.Equal(t, []event.AwareScheduler{ - scheds[1], scheds[2], - scheds[4], scheds[5], - }, sim.schedulers) - - sim.Run(ctx) - - order := []int{} - // enqueue an action on 4 (that will be executed after action enqueue on 2) - // this action will enqueue a new action on 1 (that will be executed later) - scheds[4].EnqueueAction(ctx, event.BasicAction(func(context.Context) { - order = append(order, 1) - scheds[1].EnqueueAction(ctx, event.BasicAction(func(context.Context) { - order = append(order, 2) - })) - })) - scheds[2].EnqueueAction(ctx, event.BasicAction(func(context.Context) { - order = append(order, 0) - })) - - sim.Run(ctx) - - require.Len(t, order, 3) - for i, e := range order { - require.Equal(t, i, e) - } - - order = []int{} - event.ScheduleActionIn(ctx, scheds[1], time.Minute, - event.BasicAction(func(context.Context) { - order = append(order, 3) - })) - event.ScheduleActionIn(ctx, scheds[2], time.Second, - event.BasicAction(func(context.Context) { - order = append(order, 1) - scheds[1].EnqueueAction(ctx, event.BasicAction(func(context.Context) { - order = append(order, 2) - })) - })) - scheds[4].EnqueueAction(ctx, event.BasicAction(func(context.Context) { - order = append(order, 0) - })) - - sim.Run(ctx) - - require.Len(t, order, 4) - for i, e := range order { - require.Equal(t, i, e) - } -} diff --git a/util/tracing.go b/util/tracing.go deleted file mode 100644 index 72b7ad6..0000000 --- a/util/tracing.go +++ /dev/null @@ -1,13 +0,0 @@ -package util - -import ( - "context" - "fmt" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" -) - -func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { - return otel.Tracer("go-kademlia").Start(ctx, fmt.Sprintf("KademliaDHT.%s", name), opts...) -}