From 582ab4ac13e47d2108a43cd6eaaff9db1fb9d5fb Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 9 Sep 2020 11:39:13 +0100 Subject: [PATCH] All: Implemented more reliable way to sync device and server clocks that would work with filesystem sync too --- ReactNativeClient/lib/file-api.js | 42 +++++++++++++++++++ .../lib/services/synchronizer/LockHandler.ts | 3 +- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js index f53e97ff5..52200673d 100644 --- a/ReactNativeClient/lib/file-api.js +++ b/ReactNativeClient/lib/file-api.js @@ -6,6 +6,7 @@ const JoplinError = require('lib/JoplinError'); const ArrayUtils = require('lib/ArrayUtils'); const { time } = require('lib/time-utils.js'); const { sprintf } = require('sprintf-js'); +const Mutex = require('async-mutex').Mutex; function requestCanBeRepeated(error) { const errorCode = typeof error === 'object' && error.code ? error.code : null; @@ -57,6 +58,47 @@ class FileApi { this.tempDirName_ = null; this.driver_.fileApi_ = this; this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver + this.remoteDateOffset_ = 0; + this.remoteDateNextCheckTime_ = 0; + this.remoteDateMutex_ = new Mutex(); + } + + // Approximates the current time on the sync target. It caches the time offset to + // improve performance. + async remoteDate() { + const shouldSyncTime = () => { + return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_; + }; + + if (shouldSyncTime()) { + const release = await this.remoteDateMutex_.acquire(); + + try { + // Another call might have refreshed the time while we were waiting for the mutex, + // so check again if we need to refresh. + if (shouldSyncTime()) { + const tempFile = `${this.tempDirName()}/timeCheck${Math.random() * 10000}.txt`; + const startTime = Date.now(); + await this.put(tempFile, 'timeCheck'); + const stat = await this.stat(tempFile); + const endTime = Date.now(); + const expectedTime = Math.round((endTime + startTime) / 2); + this.remoteDateOffset_ = stat.updated_time.getTime() - expectedTime; + // The sync target clock should rarely change but the device one might, + // so we need to refresh relatively frequently. + this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000; + this.delete(tempFile); // No need to await for this call + } + } catch (error) { + this.logger().warn('Could not retrieve remote date - defaulting to device date:', error); + this.remoteDateOffset_ = 0; + this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000; + } finally { + release(); + } + } + + return new Date(Date.now() + this.remoteDateOffset_); } // Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but diff --git a/ReactNativeClient/lib/services/synchronizer/LockHandler.ts b/ReactNativeClient/lib/services/synchronizer/LockHandler.ts index 62dddd7ed..17e955a56 100644 --- a/ReactNativeClient/lib/services/synchronizer/LockHandler.ts +++ b/ReactNativeClient/lib/services/synchronizer/LockHandler.ts @@ -1,5 +1,4 @@ import { Dirnames } from './utils/types'; -import ntpDate from 'lib/ntpDate'; const JoplinError = require('lib/JoplinError'); const { time } = require('lib/time-utils'); @@ -114,7 +113,7 @@ export default class LockHandler { // of that type instead. async activeLock(lockType:LockType, clientType:string = null, clientId:string = null) { const locks = await this.locks(lockType); - const currentDate = await ntpDate(); + const currentDate = await this.api_.remoteDate(); if (lockType === LockType.Exclusive) { const activeLocks = locks