2022-11-04 09:52:27 +00:00
when ( NimMajor , NimMinor ) < ( 1 , 4 ) :
{. push raises : [ Defect ] . }
else :
{. push raises : [ ] . }
2022-10-20 16:09:40 +00:00
import
2022-11-23 09:08:00 +00:00
std / options ,
2022-10-20 16:09:40 +00:00
stew / results ,
chronicles ,
chronos ,
metrics ,
bearssl / rand
import
2023-02-06 09:03:30 +00:00
.. / .. / node / peer_manager ,
2022-10-20 16:09:40 +00:00
.. / .. / utils / requests ,
. / protocol_metrics ,
2022-11-09 17:50:18 +00:00
. / common ,
2022-10-20 16:09:40 +00:00
. / rpc ,
2022-11-23 09:08:00 +00:00
. / rpc_codec
when defined ( waku_exp_store_resume ) :
import std / [ sequtils , times ]
import .. / waku_archive
2022-10-20 16:09:40 +00:00
logScope :
2022-11-03 15:36:24 +00:00
topics = " waku store client "
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
const DefaultPageSize * : uint = 20 # A recommended default number of waku messages per page
2022-11-09 17:50:18 +00:00
2022-10-20 16:09:40 +00:00
type WakuStoreClient * = ref object
peerManager : PeerManager
rng : ref rand . HmacDrbgContext
2022-11-23 09:08:00 +00:00
# TODO: Move outside of the client
when defined ( waku_exp_store_resume ) :
store : ArchiveDriver
2022-10-20 16:09:40 +00:00
proc new * ( T : type WakuStoreClient ,
peerManager : PeerManager ,
2022-11-23 09:08:00 +00:00
rng : ref rand . HmacDrbgContext ) : T =
WakuStoreClient ( peerManager : peerManager , rng : rng )
2022-10-20 16:09:40 +00:00
2022-11-09 17:50:18 +00:00
proc sendHistoryQueryRPC ( w : WakuStoreClient , req : HistoryQuery , peer : RemotePeerInfo ) : Future [ HistoryResult ] {. async , gcsafe . } =
2022-10-20 16:09:40 +00:00
let connOpt = await w . peerManager . dialPeer ( peer , WakuStoreCodec )
if connOpt . isNone ( ) :
waku_store_errors . inc ( labelValues = [ dialFailure ] )
2022-11-09 17:50:18 +00:00
return err ( HistoryError ( kind : HistoryErrorKind . PEER_DIAL_FAILURE , address : $ peer ) )
2022-11-21 08:36:41 +00:00
2022-10-20 16:09:40 +00:00
let connection = connOpt . get ( )
2022-11-17 19:40:08 +00:00
let reqRpc = HistoryRPC ( requestId : generateRequestId ( w . rng ) , query : some ( req . toRPC ( ) ) )
2022-11-09 17:50:18 +00:00
await connection . writeLP ( reqRpc . encode ( ) . buffer )
2022-10-20 16:09:40 +00:00
2022-11-09 17:50:18 +00:00
let buf = await connection . readLp ( MaxRpcSize . int )
let respDecodeRes = HistoryRPC . decode ( buf )
if respDecodeRes . isErr ( ) :
2022-10-20 16:09:40 +00:00
waku_store_errors . inc ( labelValues = [ decodeRpcFailure ] )
2022-11-09 17:50:18 +00:00
return err ( HistoryError ( kind : HistoryErrorKind . BAD_RESPONSE , cause : decodeRpcFailure ) )
let respRpc = respDecodeRes . get ( )
# Disabled ,for now, since the default response is a possible case (no messages, pagesize = 0, error = NONE(0))
# TODO: Rework the RPC protocol to differentiate the default value from an empty value (e.g., status = 200 (OK))
# and rework the protobuf parsing to return Option[T] when empty values are received
2022-11-17 19:40:08 +00:00
if respRpc . response . isNone ( ) :
waku_store_errors . inc ( labelValues = [ emptyRpcResponseFailure ] )
return err ( HistoryError ( kind : HistoryErrorKind . BAD_RESPONSE , cause : emptyRpcResponseFailure ) )
2022-11-09 17:50:18 +00:00
2022-11-17 19:40:08 +00:00
let resp = respRpc . response . get ( )
2022-11-09 17:50:18 +00:00
return resp . toAPI ( )
2022-10-20 16:09:40 +00:00
2022-11-09 17:50:18 +00:00
proc query * ( w : WakuStoreClient , req : HistoryQuery , peer : RemotePeerInfo ) : Future [ HistoryResult ] {. async , gcsafe . } =
return await w . sendHistoryQueryRPC ( req , peer )
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
# TODO: Move outside of the client
when defined ( waku_exp_store_resume ) :
## Resume store
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
const StoreResumeTimeWindowOffset : Timestamp = getNanosecondTime ( 20 ) ## Adjust the time window with an offset of 20 seconds
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
proc new * ( T : type WakuStoreClient ,
peerManager : PeerManager ,
rng : ref rand . HmacDrbgContext ,
store : ArchiveDriver ) : T =
WakuStoreClient ( peerManager : peerManager , rng : rng , store : store )
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
proc queryAll ( w : WakuStoreClient , query : HistoryQuery , peer : RemotePeerInfo ) : Future [ WakuStoreResult [ seq [ WakuMessage ] ] ] {. async , gcsafe . } =
## A thin wrapper for query. Sends the query to the given peer. when the query has a valid pagingInfo,
## it retrieves the historical messages in pages.
## Returns all the fetched messages, if error occurs, returns an error string
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
# Make a copy of the query
var req = query
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
var messageList : seq [ WakuMessage ] = @ [ ]
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
while true :
let queryRes = await w . query ( req , peer )
if queryRes . isErr ( ) :
return err ( $ queryRes . error )
2022-11-09 17:50:18 +00:00
2022-11-23 09:08:00 +00:00
let response = queryRes . get ( )
2022-11-09 17:50:18 +00:00
2022-11-23 09:08:00 +00:00
messageList . add ( response . messages )
2022-11-09 17:50:18 +00:00
2022-11-23 09:08:00 +00:00
# Check whether it is the last page
if response . cursor . isNone ( ) :
break
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
# Update paging cursor
req . cursor = response . cursor
2022-10-20 16:09:40 +00:00
2022-11-23 09:08:00 +00:00
return ok ( messageList )
proc queryLoop ( w : WakuStoreClient , req : HistoryQuery , peers : seq [ RemotePeerInfo ] ) : Future [ WakuStoreResult [ seq [ WakuMessage ] ] ] {. async , gcsafe . } =
## Loops through the peers candidate list in order and sends the query to each
##
## Once all responses have been received, the retrieved messages are consolidated into one deduplicated list.
## if no messages have been retrieved, the returned future will resolve into a result holding an empty seq.
let queryFuturesList = peers . mapIt ( w . queryAll ( req , it ) )
2022-10-28 09:51:46 +00:00
2022-11-23 09:08:00 +00:00
await allFutures ( queryFuturesList )
let messagesList = queryFuturesList
. map ( proc ( fut : Future [ WakuStoreResult [ seq [ WakuMessage ] ] ] ) : seq [ WakuMessage ] =
try :
# fut.read() can raise a CatchableError
# These futures have been awaited before using allFutures(). Call completed() just as a sanity check.
if not fut . completed ( ) or fut . read ( ) . isErr ( ) :
return @ [ ]
fut . read ( ) . value
except CatchableError :
return @ [ ]
)
. concat ( )
. deduplicate ( )
return ok ( messagesList )
proc put ( store : ArchiveDriver , pubsubTopic : PubsubTopic , message : WakuMessage ) : Result [ void , string ] =
let
digest = waku_archive . computeDigest ( message )
receivedTime = if message . timestamp > 0 : message . timestamp
else : getNanosecondTime ( getTime ( ) . toUnixFloat ( ) )
store . put ( pubsubTopic , message , digest , receivedTime )
proc resume * ( w : WakuStoreClient ,
peerList = none ( seq [ RemotePeerInfo ] ) ,
pageSize = DefaultPageSize ,
pubsubTopic = DefaultPubsubTopic ) : Future [ WakuStoreResult [ uint64 ] ] {. async , gcsafe . } =
## resume proc retrieves the history of waku messages published on the default waku pubsub topic since the last time the waku store node has been online
## messages are stored in the store node's messages field and in the message db
## the offline time window is measured as the difference between the current time and the timestamp of the most recent persisted waku message
## an offset of 20 second is added to the time window to count for nodes asynchrony
## peerList indicates the list of peers to query from.
## The history is fetched from all available peers in this list and then consolidated into one deduplicated list.
## Such candidates should be found through a discovery method (to be developed).
## if no peerList is passed, one of the peers in the underlying peer manager unit of the store protocol is picked randomly to fetch the history from.
## The history gets fetched successfully if the dialed peer has been online during the queried time window.
## the resume proc returns the number of retrieved messages if no error occurs, otherwise returns the error string
# If store has not been provided, don't even try
if w . store . isNil ( ) :
return err ( " store not provided (nil) " )
# NOTE: Original implementation is based on the message's sender timestamp. At the moment
# of writing, the sqlite store implementation returns the last message's receiver
# timestamp.
# lastSeenTime = lastSeenItem.get().msg.timestamp
let
lastSeenTime = w . store . getNewestMessageTimestamp ( ) . get ( Timestamp ( 0 ) )
now = getNanosecondTime ( getTime ( ) . toUnixFloat ( ) )
debug " resuming with offline time window " , lastSeenTime = lastSeenTime , currentTime = now
let
queryEndTime = now + StoreResumeTimeWindowOffset
queryStartTime = max ( lastSeenTime - StoreResumeTimeWindowOffset , 0 )
let req = HistoryQuery (
pubsubTopic : some ( pubsubTopic ) ,
startTime : some ( queryStartTime ) ,
endTime : some ( queryEndTime ) ,
pageSize : uint64 ( pageSize ) ,
ascending : true
2022-10-20 16:09:40 +00:00
)
2022-11-23 09:08:00 +00:00
var res : WakuStoreResult [ seq [ WakuMessage ] ]
if peerList . isSome ( ) :
debug " trying the candidate list to fetch the history "
res = await w . queryLoop ( req , peerList . get ( ) )
else :
debug " no candidate list is provided, selecting a random peer "
# if no peerList is set then query from one of the peers stored in the peer manager
2023-01-26 09:20:20 +00:00
let peerOpt = w . peerManager . selectPeer ( WakuStoreCodec )
2022-11-23 09:08:00 +00:00
if peerOpt . isNone ( ) :
warn " no suitable remote peers "
waku_store_errors . inc ( labelValues = [ peerNotFoundFailure ] )
return err ( " no suitable remote peers " )
debug " a peer is selected from peer manager "
res = await w . queryAll ( req , peerOpt . get ( ) )
if res . isErr ( ) :
debug " failed to resume the history "
return err ( " failed to resume the history " )
# Save the retrieved messages in the store
var added : uint = 0
for msg in res . get ( ) :
let putStoreRes = w . store . put ( pubsubTopic , msg )
if putStoreRes . isErr ( ) :
continue
added . inc ( )
return ok ( added )