Replies: 4 comments 6 replies
-
Looks great a couple thoughts
|
Beta Was this translation helpful? Give feedback.
-
We've run into a challenge integrating the P2P sync functionality with the existing code. The plan was to implement P2P sync in its own We currently see 4 possible approaches to working around this:
|
Beta Was this translation helpful? Give feedback.
-
With transmitting changes
Without transmitting changes
|
Beta Was this translation helpful? Give feedback.
-
We should make sure we handle the following scenario.
cc @pld |
Beta Was this translation helpful? Give feedback.
-
P2P Sync Technical Implementation Plan
In order to implement P2P sync, a number of different modules will need to be updated or created.
Android FHIR Engine
The biggest challenge around implementing P2P sync comes from the way that server sync has been implemented in the Android FHIR engine. Changes to resources are synced to the server using the
patch
interaction rather than theupdate
interaction which means that when a peer is syncing to the server after having synced from another peer, it needs the list of changes which were made to the resource rather than the latest version of the resource itself. So when it comes to implementing P2P sync it means that the resource and the local changes need to be synchronized between peers, and stored in the local database. To support this, some functions will need to be added to theDatabase
interface.The planned approach is to sync the locally updated version of the resource along with the local changes to the resource from the sender to the receiver, and then store them as-is on the receiver side.
On the sender side there will need to be a way to query for the local changes which have been made to particular resource, similar to the existing
getAllLocalChanges
function in theDatabase
interface. The function signature would look something likesuspend fun getLocalChanges(clazz: Class<R>, id: String): SquashedLocalChange
.On the receiver side there will need to be something similar to the existing
insertRemote
function in theDatabase
interface, which bypasses the creation of local changes and simply overwrites the resource with the version received from the server. In this case the function signature would need to look something likesuspend fun <R : Resource> updateRemote(resource: R, changes: List<LocalChangeEntity>)
so that the resource and local changes can be stored atomically within the same transaction.An alternative which was considered was keeping track of the version of the resource from the server alongside the locally updated version, and to send that original version of the resource along with the local changes so that they can be re-applied on the receiver side using a
patch
function without bypassing the existing mechanism to generate the local changes. The downside of this approach is the loss of efficiency due to having to store multiple versions of each resource, and having to re-apply the patches during sync. Storing the reverse patch for each update and applying the reverse patches to get back to the original resource may reduce the amount of additional storage needed, but it would increase the amount of processing required during sync.P2P Sync Module
Payload
A new class is needed to encapsulate the data which is exchanged between the sender and receiver during sync.
The class needs to be serializable so that it can be sent over different communication channels such as a socket.
It will need to include:
LocalChangeEntity
should probably be used instead of theLocalChangeEntity
class itself since fields other thantype
andpayload
are not relevant, but this can be decided during development.Sender
The module will provide a
SyncSenderSession
interface which can be implemented for different communication channels (Wi-Fi Direct, Nearby API, etc.). It is responsible for negotiating the parameters which are to be used for the sync, and for passing the sync payloads over the wire to the receiver.It uses
kotlinx.coroutines.flow.Flow
to provide a backpressure mechanism so that the receiver does not get overwhelmed when sending large numbers of resources.To do the work of sending the resources, the module will also implement a
SyncSender
class which is responsible for retrieving the resources and local changes from aFhirEngine
and aDatabase
instance, and passing them to aSyncSenderSession
instance. A sender will be instantiated by the app when a sync is performed, and it will report progress and completion back to the app.The procedure which the sender will follow is:
initiate
on theSyncSenderSession
. This will get back theResourceSyncParams
from the receiver so that the sender knows which resources to send._lastUpdated
timestamp is excluded from this matching because resources which haven't been updated from the server may still have local changes which need to be synced. Pagination or streaming should be used when selecting all of the resources so that they're not loaded into memory all at once since their could be a very large number of them.send
on theSyncSenderSession
withFlow
builder block in which the sync payloads will be emitted.Receiver
To mirror the
SyncSenderSession
, the module will provide aSyncReceiverSession
interface which can be implemented for different communication channels to handle the receiving of resources from the sender.The
receive
function will once again return aFlow
to provide a backpressure mechanism.The module will provide a
ConflictResolutionStrategy
interface which is used for determining how to deal with conflicts in the case where both peers have local changes to a resource.Implementations of two strategies will be provided by default:
TheirsConflictResolutionStrategy
which accepts the other peer's version of the resource in the case of a conflict.OursConflictResolutionStrategy
which accepts this peer's version of the resource in the case of a conflict.These two strategies effectively apply directionality to the sync. The terminology of "theirs" and "ours" is borrowed from Git.
Additional custom strategies can be implemented within the apps that need them if merging of payloads is possible using some implementation-specified heuristics.
To do the work of receiving the resources, the module will also implement a
SyncReceiver
class which collects sync payloads from the sync receiver session and processes them by resolving conflicts using the specified strategy, and then persisting them to the local database. A receiver will be instantiated by the app when a sync is performed, and once the sync has been completed it will return a result containing references to the resources which were successfully processed, and to those which were not processed due to conflicts. These can then be reported back to the user at resource type or individual resource level.The procedure which the receiver will follow is:
receive
on theSyncReceiverSession
to begin the resource synchronization process.ResourceSyncParams
.Wi-Fi Direct P2P Sync Module
After establishing a connection between peers, Wi-Fi Direct allows the peers to communicate with one another over a
java.net.Socket
. This is a low-level primitive, and some form of messaging protocol needs to be implemented on top of it in order to exchange data in a meaningful way. For the sake of simplicity this module will implement sync using JSON as the serialization format, and use aBufferedWriter
to separate individual messages by newline characters. This can be made more efficient in future if the need arises, by implementing a more compact serialization format such as Protocol Buffers, and smarter message framing.The module will exchange control messages over the socket to perform authentication, and to agree on sync parameters, and communicate state between sender and receiver.
It will provide a
SocketSyncSenderSession
implementation of theSyncSenderSession
interface. Wheninitiate
is called by theSyncSender
class, it will write a message to the socket, providing an authentication challenge and some metadata to the receiver such as the version of the protocol and the configured FHIR server URL. It will then wait for the receiver to respond by accepting the sync with the authentication response and the resource sync parameters, or by rejecting the sync with a reason (such as mismatched versions or FHIR server URLs). If the authentication response is valid it will wait for thesend
function to be called, otherwise it will notify the receiver and close the socket. Whensend
is then called by theSyncSender
class, it will collect the flow by serializing each payload and writing it to the underlying socket. Once the flow ends it will send a message to the receiver to notify it, and then close the socket.It will provide a
SocketSyncReceiverSession
implementation of theSyncReceiverSession
interface. Whenreceive
is called it will start reading from the socket and wait for the initiation message from the sender to be received. It will then compare the metadata from the sender with its own, prompt the user to provide an authentication code, and respond with the configured resource sync parameters. It will then wait to either receive an authentication failure message from the sender, or to being receiving sync payloads. For each payload received, it will deserialize it, and emit it to the flow so that it can be handled by theSyncReceiver
class. Once the completion message has been received from the sender, it will close the socket and end the flow.The module may also need to provide interface components for consumption in implementing apps as Fragments and Activities.
FHIR Core
Changes will need to be made to the FHIR Core repository to make use of the Wi-Fi Direct P2P Sync Module in the apps which require a P2P sync feature. This will entail injecting the required dependencies into the classes provided by the module, specifying some configuration, and making use of the UI components.
Beta Was this translation helpful? Give feedback.
All reactions