2
0
Fork 0
Vmeda.Online/src/providers/cron.ts

464 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

// (C) Copyright 2015 Martin Dougiamas
//
// 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 { Injectable } from '@angular/core';
import { Network } from '@ionic-native/network';
import { CoreAppProvider } from './app';
import { CoreConfigProvider } from './config';
import { CoreLoggerProvider } from './logger';
import { CoreUtilsProvider } from './utils/utils';
import { CoreConstants } from '../core/constants';
import { SQLiteDB } from '../classes/sqlitedb';
export interface CoreCronHandler {
name: string; // Handler's name.
getInterval?(): number; // Returns handler's interval in milliseconds. Defaults to CoreCronDelegate.DEFAULT_INTERVAL.
usesNetwork?(): boolean; // Whether the process uses network or not. True if not defined.
isSync?(): boolean; // Whether it's a synchronization process or not. True if not defined.
canManualSync?(): boolean; // Whether the sync can be executed manually. Call isSync if not defined.
execute?(siteId?: string): Promise<any>; // Execute the process. Receives ID of site affected, undefined for all sites.
// Important: If the promise is rejected then this function will be called again
// often, it shouldn't be abused.
running: boolean; // Whether the handler is running. Used internally by the provider, there's no need to set it.
timeout: number; // Timeout ID for the handler scheduling. Used internally by the provider, there's no need to set it.
};
/*
* Service to handle cron processes. The registered processes will be executed every certain time.
*/
@Injectable()
export class CoreCronDelegate {
// Constants.
public static DEFAULT_INTERVAL = 3600000; // Default interval is 1 hour.
public static MIN_INTERVAL = 300000; // Minimum interval is 5 minutes.
public static DESKTOP_MIN_INTERVAL = 60000; // Minimum interval in desktop is 1 minute.
public static MAX_TIME_PROCESS = 120000; // Max time a process can block the queue. Defaults to 2 minutes.
// Variables for database.
protected CRON_TABLE = 'cron';
protected tableSchema = {
name: this.CRON_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
},
{
name: 'value',
type: 'INTEGER'
}
]
};
protected logger;
protected appDB: SQLiteDB;
protected handlers: {[s: string]: CoreCronHandler} = {};
protected queuePromise = Promise.resolve();
constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private configProvider: CoreConfigProvider,
private utils: CoreUtilsProvider, network: Network) {
this.logger = logger.getInstance('CoreCronDelegate');
this.appDB = this.appProvider.getDB();
this.appDB.createTableFromSchema(this.tableSchema);
// When the app is re-connected, start network handlers that were stopped.
network.onConnect().subscribe(() => {
this.startNetworkHandlers();
});
}
/**
* Try to execute a handler. It will schedule the next execution once done.
* If the handler cannot be executed or it fails, it will be re-executed after mmCoreCronMinInterval.
*
* @param {string} name Name of the handler.
* @param {boolean} [force] Wether the execution is forced (manual sync).
* @param {string} [siteId] Site ID. If not defined, all sites.
* @return {Promise<any>} Promise resolved if handler is executed successfully, rejected otherwise.
*/
protected checkAndExecuteHandler(name: string, force?: boolean, siteId?: string) : Promise<any> {
if (!this.handlers[name] || !this.handlers[name].execute) {
// Invalid handler.
this.logger.debug('Cannot execute handler because is invalid: ' + name);
return Promise.reject(null);
}
let usesNetwork = this.handlerUsesNetwork(name),
isSync = !force && this.isHandlerSync(name),
promise;
if (usesNetwork && !this.appProvider.isOnline()) {
// Offline, stop executing.
this.logger.debug('Cannot execute handler because device is offline: ' + name);
this.stopHandler(name);
return Promise.reject(null);
}
if (isSync) {
// Check network connection.
promise = this.configProvider.get(CoreConstants.settingsSyncOnlyOnWifi, false).then((syncOnlyOnWifi) => {
return !syncOnlyOnWifi || !this.appProvider.isNetworkAccessLimited();
});
} else {
promise = Promise.resolve(true);
}
return promise.then((execute: boolean) => {
if (!execute) {
// Cannot execute in this network connection, retry soon.
this.logger.debug('Cannot execute handler because device is using limited connection: ' + name);
this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL);
return Promise.reject(null);
}
// Add the execution to the queue.
this.queuePromise = this.queuePromise.catch(() => {
// Ignore errors in previous handlers.
}).then(() => {
return this.executeHandler(name, siteId).then(() => {
this.logger.debug(`Execution of handler '${name}' was a success.`);
return this.setHandlerLastExecutionTime(name, Date.now()).then(() => {
this.scheduleNextExecution(name);
});
}, () => {
// Handler call failed. Retry soon.
this.logger.debug(`Execution of handler '${name}' failed.`);
this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL);
return Promise.reject(null);
});
});
return this.queuePromise;
});
}
/**
* Run a handler, cancelling the execution if it takes more than MAX_TIME_PROCESS.
*
* @param {string} name Name of the handler.
* @param {string} [siteId] Site ID. If not defined, all sites.
* @return {Promise<any>} Promise resolved when the handler finishes or reaches max time, rejected if it fails.
*/
protected executeHandler(name: string, siteId?: string) : Promise<any> {
return new Promise((resolve, reject) => {
let cancelTimeout;
this.logger.debug('Executing handler: ' + name);
// Wrap the call in Promise.resolve to make sure it's a promise.
Promise.resolve(this.handlers[name].execute(siteId)).then(resolve).catch(reject).finally(() => {
clearTimeout(cancelTimeout);
});
cancelTimeout = setTimeout(() => {
// The handler took too long. Resolve because we don't want to retry soon.
this.logger.debug(`Resolving execution of handler '${name}' because it took too long.`);
resolve();
}, CoreCronDelegate.MAX_TIME_PROCESS);
});
}
/**
* Force execution of synchronization cron tasks without waiting for the scheduled time.
* Please notice that some tasks may not be executed depending on the network connection and sync settings.
*
* @param {string} [siteId] Site ID. If not defined, all sites.
* @return {Promise<any>} Promise resolved if all handlers are executed successfully, rejected otherwise.
*/
forceSyncExecution(siteId?: string) : Promise<any> {
let promises = [];
for (let name in this.handlers) {
let handler = this.handlers[name];
if (this.isHandlerManualSync(name)) {
// Mark the handler as running (it might be running already).
handler.running = true;
// Cancel pending timeout.
clearTimeout(handler.timeout);
delete handler.timeout;
// Now force the execution of the handler.
promises.push(this.checkAndExecuteHandler(name, true, siteId));
}
}
return this.utils.allPromises(promises);
}
/**
* Get a handler's interval.
*
* @param {string} name Handler's name.
* @return {number} Handler's interval.
*/
protected getHandlerInterval(name) : number {
if (!this.handlers[name] || !this.handlers[name].getInterval) {
// Invalid, return default.
return CoreCronDelegate.DEFAULT_INTERVAL;
}
// Don't allow intervals lower than the minimum.
const minInterval = this.appProvider.isDesktop() ? CoreCronDelegate.DESKTOP_MIN_INTERVAL : CoreCronDelegate.MIN_INTERVAL,
handlerInterval = this.handlers[name].getInterval();
if (!handlerInterval) {
return CoreCronDelegate.DEFAULT_INTERVAL;
} else {
return Math.max(minInterval, handlerInterval);
}
}
/**
* Get a handler's last execution ID.
*
* @param {string} name Handler's name.
* @return {string} Handler's last execution ID.
*/
protected getHandlerLastExecutionId(name: string) : string {
return 'last_execution_' + name;
}
/**
* Get a handler's last execution time. If not defined, return 0.
*
* @param {string} name Handler's name.
* @return {Promise<number>} Promise resolved with the handler's last execution time.
*/
protected getHandlerLastExecutionTime(name: string) : Promise<number> {
const id = this.getHandlerLastExecutionId(name);
return this.appDB.getRecord(this.CRON_TABLE, {id: id}).then((entry) => {
const time = parseInt(entry.value, 10);
return isNaN(time) ? 0 : time;
}).catch(() => {
return 0; // Not set, return 0.
});
}
/**
* Check if a handler uses network. Defaults to true.
*
* @param {string} name Handler's name.
* @return {boolean} True if handler uses network or not defined, false otherwise.
*/
protected handlerUsesNetwork(name: string) : boolean {
if (!this.handlers[name] || !this.handlers[name].usesNetwork) {
// Invalid, return default.
return true;
}
return this.handlers[name].usesNetwork();
}
/**
* Check if there is any manual sync handler registered.
*
* @return {boolean} Whether it has at least 1 manual sync handler.
*/
hasManualSyncHandlers() : boolean {
for (let name in this.handlers) {
if (this.isHandlerManualSync(name)) {
return true;
}
}
return false;
}
/**
* Check if there is any sync handler registered.
*
* @return {boolean} Whether it has at least 1 sync handler.
*/
hasSyncHandlers() : boolean {
for (let name in this.handlers) {
if (this.isHandlerSync(name)) {
return true;
}
}
return false;
}
/**
* Check if a handler can be manually synced. Defaults will use isSync instead.
*
* @param {string} name Handler's name.
* @return {boolean} True if handler is a sync process and can be manually executed or not defined, false otherwise.
*/
protected isHandlerManualSync(name: string) : boolean {
if (!this.handlers[name] || !this.handlers[name].canManualSync) {
// Invalid, return default.
return this.isHandlerSync(name);
}
return this.handlers[name].canManualSync();
}
/**
* Check if a handler is a sync process. Defaults to true.
*
* @param {string} name Handler's name.
* @return {boolean} True if handler is a sync process or not defined, false otherwise.
*/
protected isHandlerSync(name) : boolean {
if (!this.handlers[name] || !this.handlers[name].isSync) {
// Invalid, return default.
return true;
}
return this.handlers[name].isSync();
}
/**
* Register a handler to be executed every certain time.
*
* @param {CoreCronHandler} handler The handler to register.
*/
register(handler: CoreCronHandler) : void {
if (!handler || !handler.name) {
// Invalid handler.
return;
}
if (typeof this.handlers[handler.name] != 'undefined') {
this.logger.debug(`The cron handler '${name}' is already registered.`);
return;
}
this.logger.debug.debug(`Register handler '${name}' in cron.`);
handler.running = false;
this.handlers[handler.name] = handler;
// Start the handler.
this.startHandler(name);
}
/**
* Schedule a next execution for a handler.
*
* @param {string} name Name of the handler.
* @param {number} [time] Time to the next execution. If not supplied it will be calculated using the last execution and
* the handler's interval. This param should be used only if it's really necessary.
*/
protected scheduleNextExecution(name: string, time?: number) : void {
if (!this.handlers[name]) {
// Invalid handler.
return;
}
if (this.handlers[name].timeout) {
// There's already a pending timeout.
return;
}
let promise;
if (time) {
promise = Promise.resolve(time);
} else {
// Get last execution time to check when do we need to execute it.
promise = this.getHandlerLastExecutionTime(name).then((lastExecution) => {
const interval = this.getHandlerInterval(name),
nextExecution = lastExecution + interval;
return nextExecution - Date.now();
});
}
promise.then((nextExecution) => {
this.logger.debug(`Scheduling next execution of handler '${name}' in '${nextExecution}' ms`);
if (nextExecution < 0) {
nextExecution = 0; // Big negative numbers aren't executed immediately.
}
this.handlers[name].timeout = setTimeout(() => {
delete this.handlers[name].timeout;
this.checkAndExecuteHandler(name);
}, nextExecution);
});
}
/**
* Set a handler's last execution time.
*
* @param {string} name Handler's name.
* @param {number} time Time to set.
* @return {Promise} Promise resolved when the execution time is saved.
*/
protected setHandlerLastExecutionTime(name: string, time: number) : Promise<any> {
const id = this.getHandlerLastExecutionId(name),
entry = {
id: id,
value: time
}
return this.appDB.insertOrUpdateRecord(this.CRON_TABLE, entry, {id: id});
}
/**
* Start running a handler periodically.
*
* @param {string} name Name of the handler.
*/
protected startHandler(name) : void {
if (!this.handlers[name]) {
// Invalid handler.
this.logger.debug(`Cannot start handler '${name}', is invalid.`);
return;
}
if (this.handlers[name].running) {
this.logger.debug(`Handler '${name}', is already running.`);
return;
}
this.handlers[name].running = true;
this.scheduleNextExecution(name);
}
/**
* Start running periodically the handlers that use network.
*/
startNetworkHandlers() : void {
for (let name in this.handlers) {
if (this.handlerUsesNetwork(name)) {
this.startHandler(name);
}
}
}
/**
* Stop running a handler periodically.
*
* @param {string} name Name of the handler.
*/
protected stopHandler(name) {
if (!this.handlers[name]) {
// Invalid handler.
this.logger.debug(`Cannot stop handler '${name}', is invalid.`);
return;
}
if (!this.handlers[name].running) {
this.logger.debug(`Cannot stop handler '${name}', it's not running.`);
return;
}
this.handlers[name].running = false;
clearTimeout(this.handlers[name].timeout);
delete this.handlers[name].timeout;
}
}