Skip to content

Commit

Permalink
Adds unknown replication status
Browse files Browse the repository at this point in the history
When the user is offline but we know that all docs have been replicated to the server show "unknown" replication state.
Also refactors the replication events.

#5501
  • Loading branch information
garethbowen committed Mar 14, 2019
1 parent 6e88d10 commit 43aeaf4
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 203 deletions.
6 changes: 4 additions & 2 deletions webapp/src/css/inbox.less
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,10 @@ body {
}

.header .dropdown-menu .sync-status {
.in_progress,
.not_required {
.no-click {
color: @label-color;
}
.success {
color: @ok-color;
}
.required {
Expand Down
84 changes: 47 additions & 37 deletions webapp/src/js/controllers/inbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,56 +100,66 @@ var _ = require('underscore'),
document.title = title;
});

const SYNC_STATUS = {
inProgress: {
icon: 'fa-refresh',
key: 'sync.status.in_progress',
debounce: true
},
success: {
icon: 'fa-check',
key: 'sync.status.not_required',
className: 'success'
},
required: {
icon: 'fa-exclamation-triangle',
key: 'sync.status.required',
className: 'required'
},
unknown: {
icon: 'fa-question-circle',
key: 'sync.status.unknown'
}
};

$scope.replicationStatus = {
disabled: false,
lastSuccess: {},
lastTrigger: undefined,
current: 'unknown',
textKey: 'sync.status.unknown',
};
var SYNC_ICON = {
in_progress: 'fa-refresh',
not_required: 'fa-check',
required: 'fa-exclamation-triangle',
unknown: 'fa-question-circle',
};

const updateReplicationStatus = status => {
if ($scope.replicationStatus.current !== status) {
$scope.replicationStatus.current = status;
$scope.replicationStatus.textKey = 'sync.status.' + status;
$scope.replicationStatus.icon = SYNC_ICON[status];
}
current: SYNC_STATUS.unknown,
};

DBSync.addUpdateListener(update => {
if (update.disabled) {
DBSync.addUpdateListener(({ disabled, unknown, inProgress, to, from }) => {
if (disabled) {
$scope.replicationStatus.disabled = true;
// admins have potentially too much data so bypass local pouch
$log.debug('You have administrative privileges; not replicating');
return;
}

// Listen for directedReplicationStatus to update replicationStatus.lastSuccess
var now = Date.now();
if (update.directedReplicationStatus === 'success') {
$scope.replicationStatus.lastSuccess[update.direction] = now;
if (unknown) {
$scope.replicationStatus.current = SYNC_STATUS.unknown;
return;
}

// Listen for aggregateReplicationStatus updates
const status = update.aggregateReplicationStatus;
const now = Date.now();
const lastTrigger = $scope.replicationStatus.lastTrigger;
if (status === 'not_required' || status === 'required') {
const delay = lastTrigger ? (now - lastTrigger) / 1000 : 'unknown';
$log.info('Replication ended after ' + delay + ' seconds with status ' + status);
} else if (status === 'in_progress') {
const delay = lastTrigger ? (now - lastTrigger) / 1000 : 'unknown';
if (inProgress) {
$scope.replicationStatus.current = SYNC_STATUS.inProgress;
$scope.replicationStatus.lastTrigger = now;
const duration = lastTrigger ? (now - lastTrigger) / 1000 : 'unknown';
$log.info('Replication started after ' + duration + ' seconds since previous attempt.');
$log.info(`Replication started after ${delay} seconds since previous attempt`);
return;
}
if (to === 'success') {
$scope.replicationStatus.lastSuccess.to = now;
}
if (from === 'success') {
$scope.replicationStatus.lastSuccess.from = now;
}
if (to === 'required' || from === 'required') {
$log.info(`Replication failed after ${delay} seconds`);
$scope.replicationStatus.current = SYNC_STATUS.required;
} else {
$log.info(`Replication succeeded after ${delay} seconds`);
$scope.replicationStatus.current = SYNC_STATUS.success;
}

updateReplicationStatus(status);
});

$window.addEventListener('online', () => DBSync.setOnlineStatus(true), false);
Expand All @@ -158,7 +168,7 @@ var _ = require('underscore'),
key: 'sync-status',
callback: function() {
if (!DBSync.isSyncInProgress()) {
updateReplicationStatus('required');
$scope.replicationStatus.current = SYNC_STATUS.required;
}
},
});
Expand Down
188 changes: 86 additions & 102 deletions webapp/src/js/services/db-sync.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
const _ = require('underscore'),
READ_ONLY_TYPES = ['form', 'translations'],
READ_ONLY_IDS = ['resources', 'branding', 'service-worker-meta', 'zscore-charts', 'settings', 'partners'],
DDOC_PREFIX = ['_design/'],
SYNC_INTERVAL = 5 * 60 * 1000, // 5 minutes
META_SYNC_INTERVAL = 30 * 60 * 1000; // 30 minutes

const READ_ONLY_TYPES = ['form', 'translations'];
const READ_ONLY_IDS = ['resources', 'branding', 'service-worker-meta', 'zscore-charts', 'settings', 'partners'];
const DDOC_PREFIX = ['_design/'];
const LAST_REPLICATED_SEQ_KEY = require('../bootstrapper/purger').LAST_REPLICATED_SEQ_KEY;
const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes
const META_SYNC_INTERVAL = 30 * 60 * 1000; // 30 minutes

angular
.module('inboxServices')
Expand All @@ -29,11 +27,7 @@ angular
meta: undefined,
};

var authenticationIssue = function(errors) {
return _.findWhere(errors, { status: 401 });
};

var readOnlyFilter = function(doc) {
const readOnlyFilter = function(doc) {
// Never replicate "purged" documents upwards
const keys = Object.keys(doc);
if (keys.length === 4 &&
Expand All @@ -52,122 +46,110 @@ angular
);
};

var getOptions = function(direction) {
var options = {};
if (direction === 'to') {
options.checkpoint = 'source';
options.filter = readOnlyFilter;
const DIRECTIONS = [
{
name: 'to',
options: {
checkpoint: 'source',
filter: readOnlyFilter
},
allowed: () => Auth('can_edit').then(() => true).catch(() => false)
},
{
name: 'from',
options: {},
allowed: () => $q.resolve(true)
}

return options;
};

var replicate = function(direction) {
var options = getOptions(direction);
var remote = DB({ remote: true });
return DB()
.replicate[direction](remote, options)
.on('denied', function(err) {
// In theory this could be caused by 401s
// TODO: work out what `err` looks like and navigate to login
// when we detect it's a 401
$log.error('Denied replicating ' + direction + ' remote server', err);
})
.on('error', function(err) {
$log.error('Error replicating ' + direction + ' remote server', err);
})
.on('complete', function(info) {
if (!info.ok && authenticationIssue(info.errors)) {
Session.navigateToLogin();
}
});
};

const replicateTo = () => {
const AUTH_FAILURE = {};
return Auth('can_edit')
// not authorized to replicate to server - that's ok. Silently skip replication.to
.catch(() => AUTH_FAILURE)
.then(err => {
if (err !== AUTH_FAILURE) {
return replicate('to');
];

const replicate = function(direction) {
return direction.allowed()
.then(allowed => {
if (!allowed) {
// not authorized to replicate - that's ok, skip silently
return;
}
const remote = DB({ remote: true });
return DB()
.replicate[direction.name](remote, direction.options)
.on('denied', function(err) {
// In theory this could be caused by 401s
// TODO: work out what `err` looks like and navigate to login
// when we detect it's a 401
$log.error(`Denied replicating ${direction.name} remote server`, err);
})
.on('error', function(err) {
$log.error(`Error replicating ${direction.name} remote server`, err);
})
.on('complete', function(info) {
const authError = !info.ok &&
info.errors &&
info.errors.some(error => error.status === 401);
if (authError) {
Session.navigateToLogin();
}
})
.then(() => {
return; // success
})
.catch(err => {
$log.error(`Error replicating ${direction.name} remote server`, err);
return direction.name;
});
});
};

const sendUpdateForDirectedReplication = (func, direction) => {
return func
.then(() => {
sendUpdate({
direction,
directedReplicationStatus: 'success',
});
})
.catch(error => {
sendUpdate({
direction,
directedReplicationStatus: 'failure',
error,
});
return error;
});
};

var syncMeta = function() {
var remote = DB({ meta: true, remote: true });
var local = DB({ meta: true });
local.sync(remote);
};
const getCurrentSeq = () => DB().info().then(info => info.update_seq + '');
const getLastSyncSeq = () => $window.localStorage.getItem(LAST_REPLICATED_SEQ_KEY);

// inProgressSync prevents multiple concurrent replications
let inProgressSync;

const sync = force => {
if (!knownOnlineState && !force) {
return $q.resolve();
}

/*
Controllers need the status of each directed replication (directedReplicationStatus) and the
status of the replication as a whole (aggregateReplicationStatus).
*/
if (!inProgressSync) {
inProgressSync = $q
.all([
sendUpdateForDirectedReplication(replicate('from'), 'from'),
sendUpdateForDirectedReplication(replicateTo(), 'to'),
])
.then(results => {
const errors = _.filter(results, result => result);
if (errors.length > 0) {
$log.error('Error replicating remote server', errors);
sendUpdate({
aggregateReplicationStatus: 'required',
error: errors,
});
return;
}

syncIsRecent = true;
sendUpdate({ aggregateReplicationStatus: 'not_required' });

return DB().info()
.then(dbInfo => {
$window.localStorage.setItem(LAST_REPLICATED_SEQ_KEY, dbInfo.update_seq);
.all(DIRECTIONS.map(direction => replicate(direction)))
.then(errs => {
return getCurrentSeq().then(currentSeq => {
errs = errs.filter(err => err);
let update = { to: 'success', from: 'success' };
if (!errs.length) {
// no errors
syncIsRecent = true;
$window.localStorage.setItem(LAST_REPLICATED_SEQ_KEY, currentSeq);
} else if (currentSeq === getLastSyncSeq()) {
// no changes to send, but may have some to receive
update = { unknown: true };
} else {
// definitely need to sync something
errs.forEach(err => {
update[err] = 'required';
});
}
sendUpdate(update);
});
})
.finally(() => {
inProgressSync = undefined;
});
}

sendUpdate({ aggregateReplicationStatus: 'in_progress' });
sendUpdate({ inProgress: true });
return inProgressSync;
};

const syncMeta = function() {
const remote = DB({ meta: true, remote: true });
const local = DB({ meta: true });
local.sync(remote);
};

const sendUpdate = update => {
_.forEach(updateListeners, listener => {
listener(update);
});
updateListeners.forEach(listener => listener(update));
};

const resetSyncInterval = () => {
Expand Down Expand Up @@ -220,6 +202,8 @@ angular
*/
sync: force => {
if (Session.isOnlineOnly()) {
// online users have potentially too much data so bypass local pouch
$log.debug('You have administrative privileges; not replicating');
sendUpdate({ disabled: true });
return $q.resolve();
}
Expand Down
11 changes: 5 additions & 6 deletions webapp/src/templates/partials/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,16 @@
</li>

<li role="separator" class="divider" ng-if="!replicationStatus.disabled"></li>
<li role="presentation disabled" ng-show="!replicationStatus.disabled && replicationStatus.current !== 'unknown'">
<a role="menuitem" tabindex="-1" ng-if="replicationStatus.current !== 'in_progress'" ng-click="replicate()">
<li role="presentation disabled" ng-if="!replicationStatus.disabled && !replicationStatus.current.debounce">
<a role="menuitem" tabindex="-1" ng-click="replicate()">
<i class="fa fa-fw fa-refresh"></i>
<span translate>sync.now</span>
</a>
<span ng-bind-html="replicationStatus.lastCompleted | relativeDate"></span>
</li>
<li role="presentation disabled" ng-if="!replicationStatus.disabled" class="sync-status">
<a ng-class="replicationStatus.current" class="no-click">
<i class="fa fa-fw" ng-class="replicationStatus.icon"></i>
<span translate>{{replicationStatus.textKey}}</span>
<a class="no-click" ng-class="replicationStatus.current.className">
<i class="fa fa-fw" ng-class="replicationStatus.current.icon"></i>
<span translate>{{replicationStatus.current.key}}</span>
</a>
</li>
</ul>
Expand Down

0 comments on commit 43aeaf4

Please sign in to comment.