Merge pull request #2480 from dpalou/MOBILE-3477

MOBILE-3477 core: Reduce concurrent calls to local-notifications plugin
main
Juan Leyva 2020-08-25 16:35:47 +02:00 committed by GitHub
commit 715e1e6600
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 279 additions and 104 deletions

View File

@ -1606,10 +1606,13 @@ export class AddonCalendarProvider {
* @param siteId Site ID the event belongs to. If not defined, use current site. * @param siteId Site ID the event belongs to. If not defined, use current site.
* @return Promise resolved when the notification is scheduled. * @return Promise resolved when the notification is scheduled.
*/ */
protected scheduleEventNotification(event: AddonCalendarAnyEvent, reminderId: number, time: number, siteId?: string) protected async scheduleEventNotification(event: AddonCalendarAnyEvent, reminderId: number, time: number, siteId?: string)
: Promise<void> { : Promise<void> {
if (this.localNotificationsProvider.isAvailable()) { if (!this.localNotificationsProvider.isAvailable()) {
return;
}
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (time === 0) { if (time === 0) {
@ -1617,25 +1620,21 @@ export class AddonCalendarProvider {
return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
} }
let promise;
if (time == -1) { if (time == -1) {
// If time is -1, get event default time to calculate the notification time. // If time is -1, get event default time to calculate the notification time.
promise = this.getDefaultNotificationTime(siteId).then((time) => { time = await this.getDefaultNotificationTime(siteId);
if (time == 0) { if (time == 0) {
// Default notification time is disabled, do not show. // Default notification time is disabled, do not show.
return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
} }
return event.timestart - (time * 60); time = event.timestart - (time * 60);
});
} else {
promise = Promise.resolve(time);
} }
return promise.then((time) => {
time = time * 1000; time = time * 1000;
if (time <= new Date().getTime()) { if (time <= Date.now()) {
// This reminder is over, don't schedule. Cancel if it was scheduled. // This reminder is over, don't schedule. Cancel if it was scheduled.
return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId); return this.localNotificationsProvider.cancel(reminderId, AddonCalendarProvider.COMPONENT, siteId);
} }
@ -1656,11 +1655,6 @@ export class AddonCalendarProvider {
}; };
return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId); return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId);
});
} else {
return Promise.resolve();
}
} }
/** /**

View File

@ -0,0 +1,143 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreUtils, PromiseDefer } from '@providers/utils/utils';
/**
* Function to add to the queue.
*/
export type CoreQueueRunnerFunction<T> = (...args: any[]) => T | Promise<T>;
/**
* Queue item.
*/
export type CoreQueueRunnerItem<T = any> = {
/**
* Item ID.
*/
id: string;
/**
* Function to execute.
*/
fn: CoreQueueRunnerFunction<T>;
/**
* Deferred with a promise resolved/rejected with the result of the function.
*/
deferred: PromiseDefer;
};
/**
* Options to pass to add item.
*/
export type CoreQueueRunnerAddOptions = {
/**
* Whether to allow having multiple entries with same ID in the queue.
*/
allowRepeated?: boolean;
};
/**
* A queue to prevent having too many concurrent executions.
*/
export class CoreQueueRunner {
protected queue: {[id: string]: CoreQueueRunnerItem} = {};
protected orderedQueue: CoreQueueRunnerItem[] = [];
protected numberRunning = 0;
constructor(protected maxParallel: number = 1) { }
/**
* Get unique ID.
*
* @param id ID.
* @return Unique ID.
*/
protected getUniqueId(id: string): string {
let newId = id;
let num = 1;
do {
newId = id + '-' + num;
num++;
} while (newId in this.queue);
return newId;
}
/**
* Process next item in the queue.
*
* @return Promise resolved when next item has been treated.
*/
protected async processNextItem(): Promise<void> {
if (!this.orderedQueue.length || this.numberRunning >= this.maxParallel) {
// Queue is empty or max number of parallel runs reached, stop.
return;
}
const item = this.orderedQueue.shift();
this.numberRunning++;
try {
const result = await item.fn();
item.deferred.resolve(result);
} catch (error) {
item.deferred.reject(error);
} finally {
delete this.queue[item.id];
this.numberRunning--;
this.processNextItem();
}
}
/**
* Add an item to the queue.
*
* @param id ID.
* @param fn Function to call.
* @param options Options.
* @return Promise resolved when the function has been executed.
*/
run<T>(id: string, fn: CoreQueueRunnerFunction<T>, options?: CoreQueueRunnerAddOptions): Promise<T> {
options = options || {};
if (id in this.queue) {
if (!options.allowRepeated) {
// Item already in queue, return its promise.
return this.queue[id].deferred.promise;
}
id = this.getUniqueId(id);
}
// Add the item in the queue.
const item = {
id,
fn,
deferred: CoreUtils.instance.promiseDefer(),
};
this.queue[id] = item;
this.orderedQueue.push(item);
// Process next item if we haven't reached the max yet.
this.processNextItem();
return item.deferred.promise;
}
}

View File

@ -20,7 +20,6 @@ import { CoreFileProvider } from '@providers/file';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreLangProvider } from '@providers/lang'; import { CoreLangProvider } from '@providers/lang';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications';
import { CoreConfigConstants } from '../../../../configconstants'; import { CoreConfigConstants } from '../../../../configconstants';
import { CoreSettingsHelper } from '../../providers/helper'; import { CoreSettingsHelper } from '../../providers/helper';
@ -54,7 +53,6 @@ export class CoreSettingsGeneralPage {
protected langProvider: CoreLangProvider, protected langProvider: CoreLangProvider,
protected domUtils: CoreDomUtilsProvider, protected domUtils: CoreDomUtilsProvider,
protected pushNotificationsProvider: CorePushNotificationsProvider, protected pushNotificationsProvider: CorePushNotificationsProvider,
localNotificationsProvider: CoreLocalNotificationsProvider,
protected settingsHelper: CoreSettingsHelper) { protected settingsHelper: CoreSettingsHelper) {
// Get the supported languages. // Get the supported languages.

View File

@ -24,6 +24,7 @@ import { CoreLoggerProvider } from './logger';
import { CoreTextUtilsProvider } from './utils/text'; import { CoreTextUtilsProvider } from './utils/text';
import { CoreUtilsProvider } from './utils/utils'; import { CoreUtilsProvider } from './utils/utils';
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
import { CoreQueueRunner } from '@classes/queue-runner';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { CoreConfigConstants } from '../configconstants'; import { CoreConfigConstants } from '../configconstants';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
@ -112,6 +113,7 @@ export class CoreLocalNotificationsProvider {
protected cancelSubscription: Subscription; protected cancelSubscription: Subscription;
protected addSubscription: Subscription; protected addSubscription: Subscription;
protected updateSubscription: Subscription; protected updateSubscription: Subscription;
protected queueRunner: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477).
constructor(logger: CoreLoggerProvider, private localNotifications: LocalNotifications, private platform: Platform, constructor(logger: CoreLoggerProvider, private localNotifications: LocalNotifications, private platform: Platform,
private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private configProvider: CoreConfigProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private configProvider: CoreConfigProvider,
@ -119,6 +121,7 @@ export class CoreLocalNotificationsProvider {
eventsProvider: CoreEventsProvider, private push: Push, private zone: NgZone) { eventsProvider: CoreEventsProvider, private push: Push, private zone: NgZone) {
this.logger = logger.getInstance('CoreLocalNotificationsProvider'); this.logger = logger.getInstance('CoreLocalNotificationsProvider');
this.queueRunner = new CoreQueueRunner(10);
this.appDB = appProvider.getDB(); this.appDB = appProvider.getDB();
this.dbReady = appProvider.createTablesFromSchema(this.tablesSchema).catch(() => { this.dbReady = appProvider.createTablesFromSchema(this.tablesSchema).catch(() => {
// Ignore errors. // Ignore errors.
@ -176,9 +179,13 @@ export class CoreLocalNotificationsProvider {
* @param siteId Site ID. * @param siteId Site ID.
* @return Promise resolved when the notification is cancelled. * @return Promise resolved when the notification is cancelled.
*/ */
cancel(id: number, component: string, siteId: string): Promise<any> { async cancel(id: number, component: string, siteId: string): Promise<void> {
return this.getUniqueNotificationId(id, component, siteId).then((uniqueId) => { const uniqueId = await this.getUniqueNotificationId(id, component, siteId);
return this.localNotifications.cancel(uniqueId);
const queueId = 'cancel-' + uniqueId;
await this.queueRunner.run(queueId, () => this.localNotifications.cancel(uniqueId), {
allowRepeated: true,
}); });
} }
@ -188,28 +195,29 @@ export class CoreLocalNotificationsProvider {
* @param siteId Site ID. * @param siteId Site ID.
* @return Promise resolved when the notifications are cancelled. * @return Promise resolved when the notifications are cancelled.
*/ */
cancelSiteNotifications(siteId: string): Promise<any> { async cancelSiteNotifications(siteId: string): Promise<void> {
if (!this.isAvailable()) { if (!this.isAvailable()) {
return Promise.resolve(); return;
} else if (!siteId) { } else if (!siteId) {
return Promise.reject(null); throw new Error('No site ID supplied.');
} }
return this.localNotifications.getScheduled().then((scheduled) => { const scheduled = await this.getAllScheduled();
const ids = []; const ids = [];
const queueId = 'cancelSiteNotifications-' + siteId;
scheduled.forEach((notif) => { scheduled.forEach((notif) => {
if (typeof notif.data == 'string') { notif.data = this.parseNotificationData(notif.data);
notif.data = this.textUtils.parseJSON(notif.data);
}
if (typeof notif.data == 'object' && notif.data.siteId === siteId) { if (typeof notif.data == 'object' && notif.data.siteId === siteId) {
ids.push(notif.id); ids.push(notif.id);
} }
}); });
return this.localNotifications.cancel(ids); await this.queueRunner.run(queueId, () => this.localNotifications.cancel(ids), {
allowRepeated: true,
}); });
} }
@ -243,6 +251,15 @@ export class CoreLocalNotificationsProvider {
}); });
} }
/**
* Get all scheduled notifications.
*
* @return Promise resolved with the notifications.
*/
protected getAllScheduled(): Promise<ILocalNotification[]> {
return this.queueRunner.run('allScheduled', () => this.localNotifications.getScheduled());
}
/** /**
* Get a code to create unique notifications. If there's no code assigned, create a new one. * Get a code to create unique notifications. If there's no code assigned, create a new one.
* *
@ -359,9 +376,10 @@ export class CoreLocalNotificationsProvider {
* Check if a notification has been triggered with the same trigger time. * Check if a notification has been triggered with the same trigger time.
* *
* @param notification Notification to check. * @param notification Notification to check.
* @param useQueue Whether to add the call to the queue.
* @return Promise resolved with a boolean indicating if promise is triggered (true) or not. * @return Promise resolved with a boolean indicating if promise is triggered (true) or not.
*/ */
async isTriggered(notification: ILocalNotification): Promise<boolean> { async isTriggered(notification: ILocalNotification, useQueue: boolean = true): Promise<boolean> {
await this.dbReady; await this.dbReady;
try { try {
@ -374,9 +392,17 @@ export class CoreLocalNotificationsProvider {
return stored.at === triggered; return stored.at === triggered;
} catch (err) { } catch (err) {
if (useQueue) {
const queueId = 'isTriggered-' + notification.id;
return this.queueRunner.run(queueId, () => this.localNotifications.isTriggered(notification.id), {
allowRepeated: true,
});
} else {
return this.localNotifications.isTriggered(notification.id); return this.localNotifications.isTriggered(notification.id);
} }
} }
}
/** /**
* Notify notification click to observers. Only the observers with the same component as the notification will be notified. * Notify notification click to observers. Only the observers with the same component as the notification will be notified.
@ -405,6 +431,22 @@ export class CoreLocalNotificationsProvider {
}); });
} }
/**
* Parse some notification data.
*
* @param data Notification data.
* @return Parsed data.
*/
protected parseNotificationData(data: any): any {
if (!data) {
return {};
} else if (typeof data == 'string') {
return this.textUtils.parseJSON(data, {});
} else {
return data;
}
}
/** /**
* Process the next request in queue. * Process the next request in queue.
*/ */
@ -531,20 +573,20 @@ export class CoreLocalNotificationsProvider {
* *
* @return Promise resolved when all notifications have been rescheduled. * @return Promise resolved when all notifications have been rescheduled.
*/ */
rescheduleAll(): Promise<any> { async rescheduleAll(): Promise<void> {
// Get all the scheduled notifications. // Get all the scheduled notifications.
return this.localNotifications.getScheduled().then((notifications) => { const notifications = await this.getAllScheduled();
const promises = [];
notifications.forEach((notification) => { await Promise.all(notifications.map(async (notification) => {
// Convert some properties to the needed types. // Convert some properties to the needed types.
notification.data = notification.data ? this.textUtils.parseJSON(notification.data, {}) : {}; notification.data = this.parseNotificationData(notification.data);
promises.push(this.scheduleNotification(notification)); const queueId = 'schedule-' + notification.id;
});
return Promise.all(promises); await this.queueRunner.run(queueId, () => this.scheduleNotification(notification), {
allowRepeated: true,
}); });
}));
} }
/** /**
@ -557,17 +599,12 @@ export class CoreLocalNotificationsProvider {
* @param alreadyUnique Whether the ID is already unique. * @param alreadyUnique Whether the ID is already unique.
* @return Promise resolved when the notification is scheduled. * @return Promise resolved when the notification is scheduled.
*/ */
schedule(notification: ILocalNotification, component: string, siteId: string, alreadyUnique?: boolean): Promise<any> { async schedule(notification: ILocalNotification, component: string, siteId: string, alreadyUnique?: boolean): Promise<void> {
let promise;
if (alreadyUnique) { if (!alreadyUnique) {
promise = Promise.resolve(notification.id); notification.id = await this.getUniqueNotificationId(notification.id, component, siteId);
} else {
promise = this.getUniqueNotificationId(notification.id, component, siteId);
} }
return promise.then((uniqueId) => {
notification.id = uniqueId;
notification.data = notification.data || {}; notification.data = notification.data || {};
notification.data.component = component; notification.data.component = component;
notification.data.siteId = siteId; notification.data.siteId = siteId;
@ -585,7 +622,10 @@ export class CoreLocalNotificationsProvider {
}; };
} }
return this.scheduleNotification(notification); const queueId = 'schedule-' + notification.id;
await this.queueRunner.run(queueId, () => this.scheduleNotification(notification), {
allowRepeated: true,
}); });
} }
@ -595,9 +635,9 @@ export class CoreLocalNotificationsProvider {
* @param notification Notification to schedule. * @param notification Notification to schedule.
* @return Promise resolved when scheduled. * @return Promise resolved when scheduled.
*/ */
protected scheduleNotification(notification: ILocalNotification): Promise<any> { protected scheduleNotification(notification: ILocalNotification): Promise<void> {
// Check if the notification has been triggered already. // Check if the notification has been triggered already.
return this.isTriggered(notification).then((triggered) => { return this.isTriggered(notification, false).then((triggered) => {
// Cancel the current notification in case it gets scheduled twice. // Cancel the current notification in case it gets scheduled twice.
return this.localNotifications.cancel(notification.id).finally(() => { return this.localNotifications.cancel(notification.id).finally(() => {
if (!triggered) { if (!triggered) {