1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/rocketchat_ynh.git synced 2024-09-03 20:16:25 +02:00
rocketchat_ynh/sources/programs/server/packages/percolate_synced-cron.js
2016-04-29 16:32:48 +02:00

382 lines
32 KiB
JavaScript

(function () {
/* Imports */
var Meteor = Package.meteor.Meteor;
var _ = Package.underscore._;
var check = Package.check.check;
var Match = Package.check.Match;
var MongoInternals = Package.mongo.MongoInternals;
var Mongo = Package.mongo.Mongo;
var Log = Package.logging.Log;
/* Package-scope variables */
var SyncedCron, Later;
(function(){
////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/percolate_synced-cron/packages/percolate_synced-cron.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////
//
(function () { // 1
// 2
///////////////////////////////////////////////////////////////////////////////////// // 3
// // // 4
// packages/percolate:synced-cron/synced-cron-server.js // // 5
// // // 6
///////////////////////////////////////////////////////////////////////////////////// // 7
// // 8
// A package for running jobs synchronized across multiple processes // 1 // 9
SyncedCron = { // 2 // 10
_entries: {}, // 3 // 11
running: false, // 4 // 12
options: { // 5 // 13
//Log job run details to console // 6 // 14
log: true, // 7 // 15
// 8 // 16
logger: null, // 9 // 17
// 10 // 18
//Name of collection to use for synchronisation and logging // 11 // 19
collectionName: 'cronHistory', // 12 // 20
// 13 // 21
//Default to using localTime // 14 // 22
utc: false, // 15 // 23
// 16 // 24
//TTL in seconds for history records in collection to expire // 17 // 25
//NOTE: Unset to remove expiry but ensure you remove the index from // 18 // 26
//mongo by hand // 19 // 27
collectionTTL: 172800 // 20 // 28
}, // 21 // 29
config: function(opts) { // 22 // 30
this.options = _.extend({}, this.options, opts); // 23 // 31
} // 24 // 32
} // 25 // 33
// 26 // 34
Later = Npm.require('later'); // 27 // 35
// 28 // 36
/* // 29 // 37
Logger factory function. Takes a prefix string and options object // 30 // 38
and uses an injected `logger` if provided, else falls back to // 31 // 39
Meteor's `Log` package. // 32 // 40
// 33 // 41
Will send a log object to the injected logger, on the following form: // 34 // 42
// 35 // 43
message: String // 36 // 44
level: String (info, warn, error, debug) // 37 // 45
tag: 'SyncedCron' // 38 // 46
*/ // 39 // 47
function createLogger(prefix) { // 40 // 48
check(prefix, String); // 41 // 49
// 42 // 50
// Return noop if logging is disabled. // 43 // 51
if(SyncedCron.options.log === false) { // 44 // 52
return function() {}; // 45 // 53
} // 46 // 54
// 47 // 55
return function(level, message) { // 48 // 56
check(level, Match.OneOf('info', 'error', 'warn', 'debug')); // 49 // 57
check(message, String); // 50 // 58
// 51 // 59
var logger = SyncedCron.options && SyncedCron.options.logger; // 52 // 60
// 53 // 61
if(logger && _.isFunction(logger)) { // 54 // 62
// 55 // 63
logger({ // 56 // 64
level: level, // 57 // 65
message: message, // 58 // 66
tag: prefix // 59 // 67
}); // 60 // 68
// 61 // 69
} else { // 62 // 70
Log[level]({ message: prefix + ': ' + message }); // 63 // 71
} // 64 // 72
} // 65 // 73
} // 66 // 74
// 67 // 75
var log; // 68 // 76
// 69 // 77
Meteor.startup(function() { // 70 // 78
var options = SyncedCron.options; // 71 // 79
// 72 // 80
log = createLogger('SyncedCron'); // 73 // 81
// 74 // 82
['info', 'warn', 'error', 'debug'].forEach(function(level) { // 75 // 83
log[level] = _.partial(log, level); // 76 // 84
}); // 77 // 85
// 78 // 86
// Don't allow TTL less than 5 minutes so we don't break synchronization // 79 // 87
var minTTL = 300; // 80 // 88
// 81 // 89
// Use UTC or localtime for evaluating schedules // 82 // 90
if (options.utc) // 83 // 91
Later.date.UTC(); // 84 // 92
else // 85 // 93
Later.date.localTime(); // 86 // 94
// 87 // 95
// collection holding the job history records // 88 // 96
SyncedCron._collection = new Mongo.Collection(options.collectionName); // 89 // 97
SyncedCron._collection._ensureIndex({intendedAt: 1, name: 1}, {unique: true}); // 90 // 98
// 91 // 99
if (options.collectionTTL) { // 92 // 100
if (options.collectionTTL > minTTL) // 93 // 101
SyncedCron._collection._ensureIndex({startedAt: 1 }, // 94 // 102
{ expireAfterSeconds: options.collectionTTL } ); // 95 // 103
else // 96 // 104
log.warn('Not going to use a TTL that is shorter than:' + minTTL); // 97 // 105
} // 98 // 106
}); // 99 // 107
// 100
var scheduleEntry = function(entry) { // 101
var schedule = entry.schedule(Later.parse); // 102
entry._timer = // 103
SyncedCron._laterSetInterval(SyncedCron._entryWrapper(entry), schedule); // 104
// 105
log.info('Scheduled "' + entry.name + '" next run @' // 106
+ Later.schedule(schedule).next(1)); // 107
} // 108
// 109
// add a scheduled job // 110
// SyncedCron.add({ // 111
// name: String, //*required* unique name of the job // 112
// schedule: function(laterParser) {},//*required* when to run the job // 113
// job: function() {}, //*required* the code to run // 114
// }); // 115
SyncedCron.add = function(entry) { // 116
check(entry.name, String); // 117
check(entry.schedule, Function); // 118
check(entry.job, Function); // 119
// 120
// check // 121
if (!this._entries[entry.name]) { // 122
this._entries[entry.name] = entry; // 123
// 124
// If cron is already running, start directly. // 125
if (this.running) { // 126
scheduleEntry(entry); // 127
} // 128
} // 129
} // 130
// 131
// Start processing added jobs // 132
SyncedCron.start = function() { // 133
var self = this; // 134
// 135
Meteor.startup(function() { // 136
// Schedule each job with later.js // 137
_.each(self._entries, function(entry) { // 138
scheduleEntry(entry); // 139
}); // 140
self.running = true; // 141
}); // 142
} // 143
// 144
// Return the next scheduled date of the first matching entry or undefined // 145
SyncedCron.nextScheduledAtDate = function(jobName) { // 146
var entry = this._entries[jobName]; // 147
// 148
if (entry) // 149
return Later.schedule(entry.schedule(Later.parse)).next(1); // 150
} // 151
// 152
// Remove and stop the entry referenced by jobName // 153
SyncedCron.remove = function(jobName) { // 154
var entry = this._entries[jobName]; // 155
// 156
if (entry) { // 157
if (entry._timer) // 158
entry._timer.clear(); // 159
// 160
delete this._entries[jobName]; // 161
log.info('Removed "' + entry.name + '"'); // 162
} // 163
} // 164
// 165
// Pause processing, but do not remove jobs so that the start method will // 166
// restart existing jobs // 167
SyncedCron.pause = function() { // 168
if (this.running) { // 169
_.each(this._entries, function(entry) { // 170
entry._timer.clear(); // 171
}); // 172
this.running = false; // 173
} // 174
} // 175
// 176
// Stop processing and remove ALL jobs // 177
SyncedCron.stop = function() { // 178
_.each(this._entries, function(entry, name) { // 179
SyncedCron.remove(name); // 180
}); // 181
this.running = false; // 182
} // 183
// 184
// The meat of our logic. Checks if the specified has already run. If not, // 185
// records that it's running the job, runs it, and records the output // 186
SyncedCron._entryWrapper = function(entry) { // 187
var self = this; // 188
// 189
return function(intendedAt) { // 190
intendedAt = new Date(intendedAt.getTime()); // 191
intendedAt.setMilliseconds(0); // 192
// 193
var jobHistory = { // 194
intendedAt: intendedAt, // 195
name: entry.name, // 196
startedAt: new Date() // 197
}; // 198
// 199
// If we have a dup key error, another instance has already tried to run // 200
// this job. // 201
try { // 202
jobHistory._id = self._collection.insert(jobHistory); // 203
} catch(e) { // 204
// http://www.mongodb.org/about/contributors/error-codes/ // 205
// 11000 == duplicate key error // 206
if (e.name === 'MongoError' && e.code === 11000) { // 207
log.info('Not running "' + entry.name + '" again.'); // 208
return; // 209
} // 210
// 211
throw e; // 212
}; // 213
// 214
// run and record the job // 215
try { // 216
log.info('Starting "' + entry.name + '".'); // 217
var output = entry.job(intendedAt); // <- Run the actual job // 218
// 219
log.info('Finished "' + entry.name + '".'); // 220
self._collection.update({_id: jobHistory._id}, { // 221
$set: { // 222
finishedAt: new Date(), // 223
result: output // 224
} // 225
}); // 226
} catch(e) { // 227
log.info('Exception "' + entry.name +'" ' + e.stack); // 228
self._collection.update({_id: jobHistory._id}, { // 229
$set: { // 230
finishedAt: new Date(), // 231
error: e.stack // 232
} // 233
}); // 234
} // 235
}; // 236
} // 237
// 238
// for tests // 239
SyncedCron._reset = function() { // 240
this._entries = {}; // 241
this._collection.remove({}); // 242
this.running = false; // 243
} // 244
// 245
// --------------------------------------------------------------------------- // 246
// The following two functions are lifted from the later.js package, however // 247
// I've made the following changes: // 248
// - Use Meteor.setTimeout and Meteor.clearTimeout // 249
// - Added an 'intendedAt' parameter to the callback fn that specifies the precise // 250
// time the callback function *should* be run (so we can co-ordinate jobs) // 251
// between multiple, potentially laggy and unsynced machines // 252
// 253
// From: https://github.com/bunkat/later/blob/master/src/core/setinterval.js // 254
SyncedCron._laterSetInterval = function(fn, sched) { // 255
// 256
var t = SyncedCron._laterSetTimeout(scheduleTimeout, sched), // 257
done = false; // 258
// 259
/** // 260
* Executes the specified function and then sets the timeout for the next // 261
* interval. // 262
*/ // 263
function scheduleTimeout(intendedAt) { // 264
if(!done) { // 265
fn(intendedAt); // 266
t = SyncedCron._laterSetTimeout(scheduleTimeout, sched); // 267
} // 268
} // 269
// 270
return { // 271
// 272
/** // 273
* Clears the timeout. // 274
*/ // 275
clear: function() { // 276
done = true; // 277
t.clear(); // 278
} // 279
// 280
}; // 281
// 282
}; // 283
// 284
// From: https://github.com/bunkat/later/blob/master/src/core/settimeout.js // 285
SyncedCron._laterSetTimeout = function(fn, sched) { // 286
// 287
var s = Later.schedule(sched), t; // 288
scheduleTimeout(); // 289
// 290
/** // 291
* Schedules the timeout to occur. If the next occurrence is greater than the // 292
* max supported delay (2147483647 ms) than we delay for that amount before // 293
* attempting to schedule the timeout again. // 294
*/ // 295
function scheduleTimeout() { // 296
var now = Date.now(), // 297
next = s.next(2, now); // 298
// 299
// don't schedlue another occurence if no more exist synced-cron#41 // 300
if (! next[0]) // 301
return; // 302
// 303
var diff = next[0].getTime() - now, // 304
intendedAt = next[0]; // 305
// 306
// minimum time to fire is one second, use next occurrence instead // 307
if(diff < 1000) { // 308
diff = next[1].getTime() - now; // 309
intendedAt = next[1]; // 310
} // 311
// 312
if(diff < 2147483647) { // 313
t = Meteor.setTimeout(function() { fn(intendedAt); }, diff); // 314
} // 315
else { // 316
t = Meteor.setTimeout(scheduleTimeout, 2147483647); // 317
} // 318
} // 319
// 320
return { // 321
// 322
/** // 323
* Clears the timeout. // 324
*/ // 325
clear: function() { // 326
Meteor.clearTimeout(t); // 327
} // 328
// 329
}; // 330
// 331
}; // 332
// --------------------------------------------------------------------------- // 333
// 334
///////////////////////////////////////////////////////////////////////////////////// // 343
// 344
}).call(this); // 345
// 346
////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
/* Exports */
if (typeof Package === 'undefined') Package = {};
Package['percolate:synced-cron'] = {
SyncedCron: SyncedCron
};
})();
//# sourceMappingURL=percolate_synced-cron.js.map