2020-06-19 17:29:43 +00:00
|
|
|
## Nim-LibP2P
|
|
|
|
## Copyright (c) 2020 Status Research & Development GmbH
|
|
|
|
## Licensed under either of
|
|
|
|
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
|
|
## * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
|
|
## at your option.
|
|
|
|
## This file may not be copied, modified, or distributed except according to
|
|
|
|
## those terms.
|
|
|
|
|
2021-05-21 16:27:01 +00:00
|
|
|
{.push raises: [Defect].}
|
|
|
|
|
2020-11-19 02:06:42 +00:00
|
|
|
import std/[hashes, oids, strformat]
|
2020-08-02 10:22:49 +00:00
|
|
|
import chronicles, chronos, metrics
|
2020-06-19 17:29:43 +00:00
|
|
|
import lpstream,
|
|
|
|
../multiaddress,
|
2021-01-29 16:14:53 +00:00
|
|
|
../peerinfo,
|
|
|
|
../errors
|
2020-06-19 17:29:43 +00:00
|
|
|
|
2021-01-29 16:14:53 +00:00
|
|
|
export lpstream, peerinfo, errors
|
2020-06-19 17:29:43 +00:00
|
|
|
|
2020-08-02 10:22:49 +00:00
|
|
|
logScope:
|
2020-12-01 17:34:27 +00:00
|
|
|
topics = "libp2p connection"
|
2020-08-02 10:22:49 +00:00
|
|
|
|
2020-06-19 17:29:43 +00:00
|
|
|
const
|
2020-11-05 03:52:54 +00:00
|
|
|
ConnectionTrackerName* = "Connection"
|
2020-08-10 22:17:11 +00:00
|
|
|
DefaultConnectionTimeout* = 5.minutes
|
2020-06-19 17:29:43 +00:00
|
|
|
|
|
|
|
type
|
2021-05-21 16:27:01 +00:00
|
|
|
TimeoutHandler* = proc(): Future[void] {.gcsafe, raises: [Defect].}
|
2020-08-04 13:22:05 +00:00
|
|
|
|
2020-06-19 17:29:43 +00:00
|
|
|
Connection* = ref object of LPStream
|
2020-08-04 13:22:05 +00:00
|
|
|
activity*: bool # reset every time data is sent or received
|
|
|
|
timeout*: Duration # channel timeout if no activity
|
2020-09-09 17:12:08 +00:00
|
|
|
timerTaskFut: Future[void] # the current timer instance
|
2020-08-04 13:22:05 +00:00
|
|
|
timeoutHandler*: TimeoutHandler # timeout handler
|
2020-06-19 17:29:43 +00:00
|
|
|
peerInfo*: PeerInfo
|
|
|
|
observedAddr*: Multiaddress
|
2021-01-04 18:59:05 +00:00
|
|
|
upgraded*: Future[void]
|
2021-07-26 14:12:36 +00:00
|
|
|
tag*: string # debug tag for metrics (generally ms protocol)
|
2021-03-02 23:23:40 +00:00
|
|
|
transportDir*: Direction # The bottom level transport (generally the socket) direction
|
2020-06-19 17:29:43 +00:00
|
|
|
|
2020-08-04 13:22:05 +00:00
|
|
|
proc timeoutMonitor(s: Connection) {.async, gcsafe.}
|
2020-06-19 17:29:43 +00:00
|
|
|
|
2021-01-18 22:27:29 +00:00
|
|
|
proc isUpgraded*(s: Connection): bool =
|
|
|
|
if not isNil(s.upgraded):
|
|
|
|
return s.upgraded.finished
|
|
|
|
|
2021-03-23 06:45:25 +00:00
|
|
|
proc upgrade*(s: Connection, failed: ref CatchableError = nil) =
|
2021-01-28 03:27:33 +00:00
|
|
|
if not isNil(s.upgraded):
|
|
|
|
if not isNil(failed):
|
|
|
|
s.upgraded.fail(failed)
|
|
|
|
return
|
2021-01-18 22:27:29 +00:00
|
|
|
|
2021-01-28 03:27:33 +00:00
|
|
|
s.upgraded.complete()
|
2021-01-18 22:27:29 +00:00
|
|
|
|
|
|
|
proc onUpgrade*(s: Connection) {.async.} =
|
|
|
|
if not isNil(s.upgraded):
|
|
|
|
await s.upgraded
|
|
|
|
|
2020-09-06 08:31:47 +00:00
|
|
|
func shortLog*(conn: Connection): string =
|
2021-05-21 16:27:01 +00:00
|
|
|
try:
|
|
|
|
if conn.isNil: "Connection(nil)"
|
|
|
|
elif conn.peerInfo.isNil: $conn.oid
|
|
|
|
else: &"{shortLog(conn.peerInfo.peerId)}:{conn.oid}"
|
|
|
|
except ValueError as exc:
|
|
|
|
raiseAssert(exc.msg)
|
|
|
|
|
2020-09-06 08:31:47 +00:00
|
|
|
chronicles.formatIt(Connection): shortLog(it)
|
|
|
|
|
2020-06-19 17:29:43 +00:00
|
|
|
method initStream*(s: Connection) =
|
|
|
|
if s.objName.len == 0:
|
2021-06-14 08:26:11 +00:00
|
|
|
s.objName = ConnectionTrackerName
|
2020-06-19 17:29:43 +00:00
|
|
|
|
|
|
|
procCall LPStream(s).initStream()
|
2020-08-10 22:17:11 +00:00
|
|
|
|
2020-08-04 13:22:05 +00:00
|
|
|
doAssert(isNil(s.timerTaskFut))
|
refactor bufferstream to use a queue (#346)
This change modifies how the backpressure algorithm in bufferstream
works - in particular, instead of working byte-by-byte, it will now work
seq-by-seq.
When data arrives, it usually does so in packets - in the current
bufferstream, the packet is read then split into bytes which are fed one
by one to the bufferstream. On the reading side, the bytes are popped of
the bufferstream, again byte by byte, to satisfy `readOnce` requests -
this introduces a lot of synchronization traffic because the checks for
full buffer and for async event handling must be done for every byte.
In this PR, a queue of length 1 is used instead - this means there will
at most exist one "packet" in `pushTo`, one in the queue and one in the
slush buffer that is used to store incomplete reads.
* avoid byte-by-byte copy to buffer, with synchronization in-between
* reuse AsyncQueue synchronization logic instead of rolling own
* avoid writeHandler callback - implement `write` method instead
* simplify EOF signalling by only setting EOF flag in queue reader (and
reset)
* remove BufferStream pipes (unused)
* fixes drainBuffer deadlock when drain is called from within read loop
and thus blocks draining
* fix lpchannel init order
2020-09-10 06:19:13 +00:00
|
|
|
|
2021-01-04 18:59:05 +00:00
|
|
|
if isNil(s.upgraded):
|
|
|
|
s.upgraded = newFuture[void]()
|
|
|
|
|
2020-08-10 22:17:11 +00:00
|
|
|
if s.timeout > 0.millis:
|
2020-09-09 17:12:08 +00:00
|
|
|
trace "Monitoring for timeout", s, timeout = s.timeout
|
|
|
|
|
2020-08-10 22:17:11 +00:00
|
|
|
s.timerTaskFut = s.timeoutMonitor()
|
refactor bufferstream to use a queue (#346)
This change modifies how the backpressure algorithm in bufferstream
works - in particular, instead of working byte-by-byte, it will now work
seq-by-seq.
When data arrives, it usually does so in packets - in the current
bufferstream, the packet is read then split into bytes which are fed one
by one to the bufferstream. On the reading side, the bytes are popped of
the bufferstream, again byte by byte, to satisfy `readOnce` requests -
this introduces a lot of synchronization traffic because the checks for
full buffer and for async event handling must be done for every byte.
In this PR, a queue of length 1 is used instead - this means there will
at most exist one "packet" in `pushTo`, one in the queue and one in the
slush buffer that is used to store incomplete reads.
* avoid byte-by-byte copy to buffer, with synchronization in-between
* reuse AsyncQueue synchronization logic instead of rolling own
* avoid writeHandler callback - implement `write` method instead
* simplify EOF signalling by only setting EOF flag in queue reader (and
reset)
* remove BufferStream pipes (unused)
* fixes drainBuffer deadlock when drain is called from within read loop
and thus blocks draining
* fix lpchannel init order
2020-09-10 06:19:13 +00:00
|
|
|
if isNil(s.timeoutHandler):
|
2020-11-01 22:23:26 +00:00
|
|
|
s.timeoutHandler = proc(): Future[void] =
|
|
|
|
trace "Idle timeout expired, closing connection", s
|
|
|
|
s.close()
|
2020-08-04 13:22:05 +00:00
|
|
|
|
2020-09-21 17:48:19 +00:00
|
|
|
method closeImpl*(s: Connection): Future[void] =
|
|
|
|
# Cleanup timeout timer
|
|
|
|
trace "Closing connection", s
|
2021-01-04 18:59:05 +00:00
|
|
|
|
2020-08-04 13:22:05 +00:00
|
|
|
if not isNil(s.timerTaskFut) and not s.timerTaskFut.finished:
|
|
|
|
s.timerTaskFut.cancel()
|
2020-11-29 12:34:19 +00:00
|
|
|
s.timerTaskFut = nil
|
2020-08-04 13:22:05 +00:00
|
|
|
|
2021-01-04 18:59:05 +00:00
|
|
|
if not isNil(s.upgraded) and not s.upgraded.finished:
|
|
|
|
s.upgraded.cancel()
|
|
|
|
s.upgraded = nil
|
|
|
|
|
2020-11-01 22:23:26 +00:00
|
|
|
trace "Closed connection", s
|
2020-09-21 17:48:19 +00:00
|
|
|
|
|
|
|
procCall LPStream(s).closeImpl()
|
2020-06-19 17:29:43 +00:00
|
|
|
|
2020-07-17 15:36:48 +00:00
|
|
|
func hash*(p: Connection): Hash =
|
|
|
|
cast[pointer](p).hash
|
2020-08-04 13:22:05 +00:00
|
|
|
|
2020-11-29 12:34:19 +00:00
|
|
|
proc pollActivity(s: Connection): Future[bool] {.async.} =
|
|
|
|
if s.closed and s.atEof:
|
|
|
|
return false # Done, no more monitoring
|
|
|
|
|
|
|
|
if s.activity:
|
|
|
|
s.activity = false
|
|
|
|
return true
|
|
|
|
|
|
|
|
# Inactivity timeout happened, call timeout monitor
|
|
|
|
|
|
|
|
trace "Connection timed out", s
|
|
|
|
if not(isNil(s.timeoutHandler)):
|
|
|
|
trace "Calling timeout handler", s
|
|
|
|
|
|
|
|
try:
|
|
|
|
await s.timeoutHandler()
|
|
|
|
except CancelledError:
|
|
|
|
# timeoutHandler is expected to be fast, but it's still possible that
|
|
|
|
# cancellation will happen here - no need to warn about it - we do want to
|
|
|
|
# stop the polling however
|
|
|
|
debug "Timeout handler cancelled", s
|
|
|
|
except CatchableError as exc: # Shouldn't happen
|
|
|
|
warn "exception in timeout handler", s, exc = exc.msg
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
2020-08-04 13:22:05 +00:00
|
|
|
proc timeoutMonitor(s: Connection) {.async, gcsafe.} =
|
2020-11-01 22:23:26 +00:00
|
|
|
## monitor the channel for inactivity
|
2020-08-04 13:22:05 +00:00
|
|
|
##
|
|
|
|
## if the timeout was hit, it means that
|
|
|
|
## neither incoming nor outgoing activity
|
|
|
|
## has been detected and the channel will
|
|
|
|
## be reset
|
|
|
|
##
|
|
|
|
|
2020-11-29 12:34:19 +00:00
|
|
|
while true:
|
|
|
|
try: # Sleep at least once!
|
2020-08-04 13:22:05 +00:00
|
|
|
await sleepAsync(s.timeout)
|
2020-11-29 12:34:19 +00:00
|
|
|
except CancelledError:
|
|
|
|
return
|
2020-08-04 13:22:05 +00:00
|
|
|
|
2020-11-29 12:34:19 +00:00
|
|
|
if not await s.pollActivity():
|
|
|
|
return
|
2020-08-04 13:22:05 +00:00
|
|
|
|
|
|
|
proc init*(C: type Connection,
|
|
|
|
peerInfo: PeerInfo,
|
|
|
|
dir: Direction,
|
|
|
|
timeout: Duration = DefaultConnectionTimeout,
|
2020-11-25 19:34:48 +00:00
|
|
|
timeoutHandler: TimeoutHandler = nil,
|
|
|
|
observedAddr: MultiAddress = MultiAddress()): Connection =
|
2020-08-04 13:22:05 +00:00
|
|
|
result = C(peerInfo: peerInfo,
|
|
|
|
dir: dir,
|
|
|
|
timeout: timeout,
|
2020-11-25 19:34:48 +00:00
|
|
|
timeoutHandler: timeoutHandler,
|
|
|
|
observedAddr: observedAddr)
|
2020-08-04 13:22:05 +00:00
|
|
|
|
|
|
|
result.initStream()
|