Message Flows
DefaultLoRaDataRequestHandler:ProcessRequestAsync
Incoming message - device not cached
- The message dispatcher requests the
ILoRaDeviceRequestQueue
from theLoRaDeviceRegistry
where theLoRaRequest
can be sent to. TheLoRaDeviceRegistry
maintains an in memory cache perDevAddr
and checks, if it has a cache and if it contains a valid device matching theNwkSKey
. If it does not, theLoRaDeviceRegistry
initializes aDeviceLoaderSynchronizer
asILoRaDeviceRequestQueue
, and adds it to its cache under the prefixdevloader
. - The
DeviceLoaderSynchronizer
ctor does trigger an async initialization - Load of the devices matching the
DevAddr
is triggered - The
MessageDispatcher
receives theDeviceLoaderSynchronizer
- The original
LoRaRequest
is put onto the queue where it will wait for the device to be loaded. - The
SearchByDevAddrAsync
is calling the function and tries to get a list for that particularDevAddr
. The result is a list ofIoTHubDeviceInfo
which contains everything required to connect the device to IoT Hub as well as theNwkSKey
. - The
DeviceLoaderSynchronizer
iterates over the result and asks for each of the result item to be materialized into aLoRaDevice
. - The
LoRaDeviceFactory
creates aLoRaDevice
from theIoTHubDeviceInfo
- The
LoRaDeviceFactory
also maintains the connections per device to IoT Hub. It does that through theLoRaDeviceClientConnectionManager
whereLoRaDeviceClient
are registered perDevEUI
. - Each device is initialized to get ready for processing messages
- The initialization is triggering the load of the twins through the
LoRaDeviceClient
- Once the device is initialized, the messages for the device are dispatched
- The dispatch is putting them on the
LoRaDevice
Queue - The
LoRaDevice
will process the messages in sequence to avoid contention on the device connection and delegate the processing to theILoRaDataRequestHandler
.
Observations
- If the
DevAddr
does not match any of our registered devices, we keep theDeviceLoaderSynchronizer
in cache for 30s (which is designed to ensure that pending requests that were already on the queue, can be processed), then it's evicted. Subsequent messages after those 30s with the sameDevAddr
will keep going back to the functions API. Given the fact that a device could be registered between messages, I don't see a way to avoid that except if we are willing to accept a longer period until we recognize a device. In that case we could register a 'drop' queue for theDevAddr
that will be evicted after a longer time.
Incoming message - device cached
- The message dispatcher requests the
ILoRaDeviceRequestQueue
from theLoRaDeviceRegistry
- If the device was in cache, but does not belong to our gateway, we return a
ExternalGatewayLoRaRequestQueue
. That queue is basically dropping any messages that are sent to it. - If the device was found in the cache we return that to the
MessageDispatcher
. - The request is put onto the
LoRaDevice
's queue. - The
LoRaDevice
will process the messages in sequence to avoid contention on the device connection and delegate the processing to theILoRaDataRequestHandler
.
Join Request - OTAA
-
The
MessageDispatcher
delegates the handling of the join request to theJoinRequestMessageHandler
-
The
JoinRequestMessageHandler
is executingProcessJoinRequestAsync
and extracts: DevEUI, AppEUI and DevNonce from the join request. -
A
LoRaOperationTimeWatcher
is created to monitor the elapsed time for the join request. -
The
LoRaDeviceRegistry
is queried for the device matching the join request. -
The
LoRaDeviceRegistry
is asking the functions API to lookup the device using the DevEUI, AppEUI and the DevNonce. Also the Gateway Id is sent to allow locking the join request. see the GetDevice flow- Q: why do we send the AppEUI? It looks like that's not used for the OTAA join?
-
LoRaDeviceRegistry
creates a newJoinDeviceLoader
(unless the loader is still in the cache - valid for 30min) passing in theIoTHubDeviceInfo
. -
The
ctor
of theJoinDeviceLoader
starts a thread executingLoadAsync
-
A new
LoRaDevice
is created through the factory from theIoTHubDeviceInfo
-
LoRaDevice
is returned. -
The
LoRaDevice
is initialized. -
Twins are loaded through the
ILoRaDeviceClient
(keys were fetched from the API). -
JoinDeviceLoader
is waiting for completion of the device load process -
The
LoRaDevice
is returned to theLoRaDeviceRegistry
-
The
LoRaDevice
is returned to theJoinRequestMessageHandler
-
Validation of the join request is performed (CheckMic, DevNonce, GatewayID)
-
Keys are generated
-
Validate that we can confirm the join to the device and are within
Join_accept_delay2
for the current region. -
Writing
DevAddr
,NwkSKey
,AppSKey
,AppNonce
,DevNonce
,NetID
,Region
,PreferredGatewayID
-
If we are still in time for a valid receive window, a
JoinAccept
will be sent to the device after calculating the DR and Frequency.
Function GetDevice - OTAA
-
The LNS requests the device for a join request
-
The DeviceGetter calls
TryGetJoinInfoAndValidateAsync
-
Try to get the
JoinInfo
(containing the primary key and the desired gateway id for the device) from Redis. -
If the device was not in the cache, we use the IoT Hub
RegistryManager
to fetch the Device -
If the device exists, we fetch the twins and get the Desired GatewayId.
-
The JoinInfo is stored in Redis for 60min
-
We validate that if there is a desired gateway, the gateway processing the join request, is the desired gateway. If not, a BadRequest is returned to the LNS indicating the join failure.
-
We try to set the DevEUI:Nonce value in Redis cache to ensure, only 1 Gateway is processing the join request
-
If we did not win the race, the Gateway receives a BadRequest response
-
If we were successful we create a
IoTHubDeviceInfo
and try to acquire a lock on the DevEUI -
If we did get the lock, we delete the DevEUI key.
-
If not, we print out a warning
-
We return the
IoTHubDeviceInfo
Created: 2021-11-10