From 53220307907fb31a38001fb60b45336bd0a3dd2d Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Wed, 26 Aug 2015 18:22:32 -0300 Subject: [PATCH 1/8] First attempt at cron --- client/views/admin/adminStatistics.html | 4 ++++ i18n/en.i18n.json | 1 + .../server/functions/getAvgStatistics.coffee | 2 ++ .../server/functions/getStatistics.coffee | 3 ++- server/startup/cron.coffee | 17 +++++++++++++++++ server/startup/initialData.coffee | 6 ++++++ 6 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 server/startup/cron.coffee diff --git a/client/views/admin/adminStatistics.html b/client/views/admin/adminStatistics.html index 0ad5174396b..c0f969aec78 100644 --- a/client/views/admin/adminStatistics.html +++ b/client/views/admin/adminStatistics.html @@ -28,6 +28,10 @@ {{_ "Stats_Online_Users"}} {{statistics.onlineUsers}} + + {{_ "Stats_Away_Users"}} + {{statistics.awayUsers}} + {{_ "Stats_Offline_Users"}} {{statistics.offlineUsers}} diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index a985352dd4d..b961b273449 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -232,6 +232,7 @@ "Stats_Avg_Private_Group_Users": "Average Private Group Users", "Stats_Max_Room_Users": "Max Rooms Users", "Stats_Non_Active_Users": "Inactive Users", + "Stats_Away_Users": "Away Users", "Stats_Offline_Users": "Offline Users", "Stats_Online_Users": "Online Users", "Stats_OS_Arch": "OS Arch", diff --git a/packages/rocketchat-lib/server/functions/getAvgStatistics.coffee b/packages/rocketchat-lib/server/functions/getAvgStatistics.coffee index b76754d3f36..42bd24d210b 100644 --- a/packages/rocketchat-lib/server/functions/getAvgStatistics.coffee +++ b/packages/rocketchat-lib/server/functions/getAvgStatistics.coffee @@ -14,6 +14,7 @@ RocketChat.getAvgStatistics = -> a.activeUsers += b.activeUsers a.nonActiveUsers += b.nonActiveUsers a.onlineUsers += b.onlineUsers + a.awayUsers += b.awayUsers a.offlineUsers += b.offlineUsers a.totalRooms += b.totalRooms a.totalChannels += b.totalChannels @@ -36,6 +37,7 @@ RocketChat.getAvgStatistics = -> out.activeUsers = v.activeUsers / v.count out.nonActiveUsers = v.nonActiveUsers / v.count out.onlineUsers = v.onlineUsers / v.count + out.awayUsers = v.awayUsers / v.count out.offlineUsers = v.offlineUsers / v.count out.totalRooms = v.totalRooms / v.count out.totalChannels = v.totalChannels / v.count diff --git a/packages/rocketchat-lib/server/functions/getStatistics.coffee b/packages/rocketchat-lib/server/functions/getStatistics.coffee index b699b1a0990..37a3171c61f 100644 --- a/packages/rocketchat-lib/server/functions/getStatistics.coffee +++ b/packages/rocketchat-lib/server/functions/getStatistics.coffee @@ -10,7 +10,8 @@ RocketChat.getStatistics = -> statistics.activeUsers = Meteor.users.find({ active: true }).count() statistics.nonActiveUsers = statistics.totalUsers - statistics.activeUsers statistics.onlineUsers = Meteor.users.find({ statusConnection: 'online' }).count() - statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers + statistics.awayUsers = Meteor.users.find({ statusConnection: 'away' }).count() + statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers # Room statistics statistics.totalRooms = ChatRoom.find().count() diff --git a/server/startup/cron.coffee b/server/startup/cron.coffee new file mode 100644 index 00000000000..ca676d5cec6 --- /dev/null +++ b/server/startup/cron.coffee @@ -0,0 +1,17 @@ +Meteor.startup -> + Meteor.defer -> + + # Config and Start SyncedCron + SyncedCron.config + collectionName: 'rocketchat_cron_history' + + # Generate and save statistics every hour + SyncedCron.add + name: 'Generate and save statistics every hour', + schedule: (parser) -># parser is a later.parse object + return parser.text 'every 1 hours' + job: -> + statistics = RocketChat.saveStatistics + return + + SyncedCron.start() diff --git a/server/startup/initialData.coffee b/server/startup/initialData.coffee index 9eb3d92c4fb..9af24b91b5b 100644 --- a/server/startup/initialData.coffee +++ b/server/startup/initialData.coffee @@ -1,5 +1,11 @@ Meteor.startup -> Meteor.defer -> + # Insert server unique id if it doesn't exist + if not Settings.findOne { _id: 'uniqueID' } + Settings.insert + _id: 'uniqueID' + value: Random.id() + if not ChatRoom.findOne('name': 'general')? ChatRoom.insert _id: 'GENERAL' From 09db52d0c383bda3f2ef37f99cb57584dccc698e Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Wed, 26 Aug 2015 21:14:15 -0300 Subject: [PATCH 2/8] Add percolatesudio-synced-cron manually (no version 1.2.1 on atmosphere) --- .meteor/packages | 2 +- .meteor/versions | 4 +- .../percolatestudio-synced-cron/.gitignore | 1 + .../.npm/package/.gitignore | 1 + .../.npm/package/README | 7 + .../.npm/package/npm-shrinkwrap.json | 7 + .../percolatestudio-synced-cron/.versions | 21 ++ packages/percolatestudio-synced-cron/LICENSE | 20 ++ .../percolatestudio-synced-cron/README.md | 137 ++++++++ .../example/.meteor/.finished-upgraders | 6 + .../example/.meteor/.gitignore | 1 + .../example/.meteor/.id | 7 + .../example/.meteor/packages | 9 + .../example/.meteor/release | 1 + .../example/.meteor/versions | 46 +++ .../example/example.html | 13 + .../example/example.js | 41 +++ .../example/packages/synced-cron | 1 + .../percolatestudio-synced-cron/package.js | 20 ++ .../synced-cron-server.js | 325 ++++++++++++++++++ .../synced-cron-tests.js | 216 ++++++++++++ .../percolatestudio-synced-cron/versions.json | 83 +++++ server/startup/cron.coffee | 27 +- 23 files changed, 979 insertions(+), 17 deletions(-) create mode 100755 packages/percolatestudio-synced-cron/.gitignore create mode 100644 packages/percolatestudio-synced-cron/.npm/package/.gitignore create mode 100644 packages/percolatestudio-synced-cron/.npm/package/README create mode 100644 packages/percolatestudio-synced-cron/.npm/package/npm-shrinkwrap.json create mode 100755 packages/percolatestudio-synced-cron/.versions create mode 100755 packages/percolatestudio-synced-cron/LICENSE create mode 100755 packages/percolatestudio-synced-cron/README.md create mode 100755 packages/percolatestudio-synced-cron/example/.meteor/.finished-upgraders create mode 100755 packages/percolatestudio-synced-cron/example/.meteor/.gitignore create mode 100755 packages/percolatestudio-synced-cron/example/.meteor/.id create mode 100755 packages/percolatestudio-synced-cron/example/.meteor/packages create mode 100755 packages/percolatestudio-synced-cron/example/.meteor/release create mode 100755 packages/percolatestudio-synced-cron/example/.meteor/versions create mode 100755 packages/percolatestudio-synced-cron/example/example.html create mode 100755 packages/percolatestudio-synced-cron/example/example.js create mode 120000 packages/percolatestudio-synced-cron/example/packages/synced-cron create mode 100755 packages/percolatestudio-synced-cron/package.js create mode 100755 packages/percolatestudio-synced-cron/synced-cron-server.js create mode 100755 packages/percolatestudio-synced-cron/synced-cron-tests.js create mode 100755 packages/percolatestudio-synced-cron/versions.json diff --git a/.meteor/packages b/.meteor/packages index 0d513e0252d..230623fcbd3 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -63,7 +63,7 @@ nimble:restivus nooitaf:colors pauli:accounts-linkedin percolate:migrations -percolatestudio:synced-cron +percolate:synced-cron raix:handlebar-helpers raix:ui-dropped-event tap:i18n diff --git a/.meteor/versions b/.meteor/versions index 0a7f4470b7c..12f8c33738e 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -39,7 +39,7 @@ htmljs@1.0.4 http@1.1.0 id-map@1.0.3 idorecall:email-normalize@1.0.0 -jalik:ufs@0.2.8 +jalik:ufs@0.2.7 jalik:ufs-gridfs@0.1.0 jparker:crypto-core@0.1.0 jparker:crypto-md5@0.1.1 @@ -90,7 +90,7 @@ ordered-dict@1.0.3 pauli:accounts-linkedin@1.1.2 pauli:linkedin@1.1.2 percolate:migrations@0.7.6 -percolatestudio:synced-cron@1.1.0 +percolate:synced-cron@1.2.1 qnub:emojione@0.0.3 raix:eventemitter@0.1.3 raix:eventstate@0.0.2 diff --git a/packages/percolatestudio-synced-cron/.gitignore b/packages/percolatestudio-synced-cron/.gitignore new file mode 100755 index 00000000000..677a6fc2637 --- /dev/null +++ b/packages/percolatestudio-synced-cron/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/percolatestudio-synced-cron/.npm/package/.gitignore b/packages/percolatestudio-synced-cron/.npm/package/.gitignore new file mode 100644 index 00000000000..3c3629e647f --- /dev/null +++ b/packages/percolatestudio-synced-cron/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/percolatestudio-synced-cron/.npm/package/README b/packages/percolatestudio-synced-cron/.npm/package/README new file mode 100644 index 00000000000..3d492553a43 --- /dev/null +++ b/packages/percolatestudio-synced-cron/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/percolatestudio-synced-cron/.npm/package/npm-shrinkwrap.json b/packages/percolatestudio-synced-cron/.npm/package/npm-shrinkwrap.json new file mode 100644 index 00000000000..c40d192c454 --- /dev/null +++ b/packages/percolatestudio-synced-cron/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "later": { + "version": "1.1.6" + } + } +} diff --git a/packages/percolatestudio-synced-cron/.versions b/packages/percolatestudio-synced-cron/.versions new file mode 100755 index 00000000000..c868d531e29 --- /dev/null +++ b/packages/percolatestudio-synced-cron/.versions @@ -0,0 +1,21 @@ +base64@1.0.3 +binary-heap@1.0.3 +callback-hook@1.0.3 +check@1.0.5 +ddp@1.1.0 +ejson@1.0.6 +geojson-utils@1.0.3 +id-map@1.0.3 +json@1.0.3 +local-test:percolate:synced-cron@1.2.0 +logging@1.0.7 +meteor@1.1.5 +minimongo@1.0.7 +mongo@1.1.0 +ordered-dict@1.0.3 +percolate:synced-cron@1.2.0 +random@1.0.3 +retry@1.0.3 +tinytest@1.0.5 +tracker@1.0.6 +underscore@1.0.3 diff --git a/packages/percolatestudio-synced-cron/LICENSE b/packages/percolatestudio-synced-cron/LICENSE new file mode 100755 index 00000000000..84859725814 --- /dev/null +++ b/packages/percolatestudio-synced-cron/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Percolate Studio + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/percolatestudio-synced-cron/README.md b/packages/percolatestudio-synced-cron/README.md new file mode 100755 index 00000000000..e0cdb5c7564 --- /dev/null +++ b/packages/percolatestudio-synced-cron/README.md @@ -0,0 +1,137 @@ +# percolate:synced-cron + +A simple cron system for [Meteor](http://meteor.com). It supports syncronizing jobs between multiple processes. In other words, if you add a job that runs every hour and your deployment consists of multiple app servers, only one of the app servers will execute the job each time (whichever tries first). + +## Installation + +``` sh +$ meteor add percolate:synced-cron +``` + +## API + +### Basics + +To write a cron job, give it a unique name, a schedule an a function to run like below. SyncedCron uses the fantastic [later.js](http://bunkat.github.io/later/) library behind the scenes. A Later.js `parse` object is passed into the schedule call that gives you a huge amount of flexibility for scheduling your jobs, see the [documentation](http://bunkat.github.io/later/parsers.html#overview). + +``` js +SyncedCron.add({ + name: 'Crunch some important numbers for the marketing department', + schedule: function(parser) { + // parser is a later.parse object + return parser.text('every 2 hours'); + }, + job: function() { + var numbersCrunched = CrushSomeNumbers(); + return numbersCrunched; + } +}); +``` + +To start processing your jobs, somewhere in your project add: + +``` js +SyncedCron.start(); +``` + +### Advanced + +SyncedCron uses a collection called `cronHistory` to syncronize between processes. This also serves as a useful log of when jobs ran along with their output or error. A sample item looks like: + +``` js +{ _id: 'wdYLPBZp5zzbwdfYj', + intendedAt: Sun Apr 13 2014 17:34:00 GMT-0700 (MST), + finishedAt: Sun Apr 13 2014 17:34:01 GMT-0700 (MST), + name: 'Crunch some important numbers for the marketing department', + startedAt: Sun Apr 13 2014 17:34:00 GMT-0700 (MST), + result: '1982 numbers crunched' +} +``` + +Call `SyncedCron.nextScheduledAtDate(jobName)` to find the date that the job +referenced by `jobName` will run next. + +Call `SyncedCron.remove(jobName)` to remove and stop running the job referenced by jobName. + +Call `SyncedCron.stop()` to remove and stop all jobs. + +Call `SyncedCron.pause()` to stop all jobs without removing them. The existing jobs can be rescheduled (i.e. restarted) with `SyncedCron.start()`. + +### Configuration + +You can configure SyncedCron with the `config` method. Defaults are: + +``` js + SyncedCron.config({ + // Log job run details to console + log: true, + + // Use a custom logger function (defaults to Meteor's logging package) + logger: null + + // Name of collection to use for synchronisation and logging + collectionName: 'cronHistory', + + // Default to using localTime + utc: false, + + /* + TTL in seconds for history records in collection to expire + NOTE: Unset to remove expiry but ensure you remove the index from + mongo by hand + + ALSO: SyncedCron can't use the `_ensureIndex` command to modify + the TTL index. The best way to modify the default value of + `collectionTTL` is to remove the index by hand (in the mongo shell + run `db.cronHistory.dropIndex({startedAt: 1})`) and re-run your + project. SyncedCron will recreate the index with the updated TTL. + */ + collectionTTL: 172800 + }); +``` + +### Logging + +SyncedCron uses Meteor's `logging` package by default. If you want to use your own logger (for sending to other consumers or similar) you can do so by configuring the `logger` option. + +SyncedCron expects a function as `logger`, and will pass arguments to it for you to take action on. + +```js +var MyLogger = function(opts) { + console.log('Level', opts.level); + console.log('Message', opts.message); + console.log('Tag', opts.tag); +} + +SyncedCron.config({ + logger: MyLogger +}); + +SyncedCron.add({ name: 'Test Job', ... }); +SyncedCron.start(); +``` + +The `opts` object passed to `MyLogger` above includes `level`, `message`, and `tag`. + +- `level` will be one of `info`, `warn`, `error`, `debug`. +- `message` is something like `Scheduled "Test Job" next run @Fri Mar 13 2015 10:15:00 GMT+0100 (CET)`. +- `tag` will always be `"SyncedCron"` (handy for filtering). + + +## Caveats + +Beware, SyncedCron probably won't work as expected on certain shared hosting providers that shutdown app instances when they aren't receiving requests (like Heroku's free dyno tier or Meteor free galaxy). + +## Contributing + +Write some code. Write some tests. To run the tests, do: + +``` sh +$ meteor test-packages ./ +``` + +## License + +MIT. (c) Percolate Studio, maintained by Zoltan Olah (@zol). + +Synced Cron was developed as part of the [Verso](http://versoapp.com) project. diff --git a/packages/percolatestudio-synced-cron/example/.meteor/.finished-upgraders b/packages/percolatestudio-synced-cron/example/.meteor/.finished-upgraders new file mode 100755 index 00000000000..ee0ed5a316f --- /dev/null +++ b/packages/percolatestudio-synced-cron/example/.meteor/.finished-upgraders @@ -0,0 +1,6 @@ +# This file contains information which helps Meteor properly upgrade your +# app when you run 'meteor update'. You should check it into version control +# with your project. + +notices-for-0.9.0 +notices-for-0.9.1 diff --git a/packages/percolatestudio-synced-cron/example/.meteor/.gitignore b/packages/percolatestudio-synced-cron/example/.meteor/.gitignore new file mode 100755 index 00000000000..40830374235 --- /dev/null +++ b/packages/percolatestudio-synced-cron/example/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/packages/percolatestudio-synced-cron/example/.meteor/.id b/packages/percolatestudio-synced-cron/example/.meteor/.id new file mode 100755 index 00000000000..536ae724204 --- /dev/null +++ b/packages/percolatestudio-synced-cron/example/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +18n88nbddjnt450w00w diff --git a/packages/percolatestudio-synced-cron/example/.meteor/packages b/packages/percolatestudio-synced-cron/example/.meteor/packages new file mode 100755 index 00000000000..afc9e5d4e0c --- /dev/null +++ b/packages/percolatestudio-synced-cron/example/.meteor/packages @@ -0,0 +1,9 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +autopublish +insecure +percolate:synced-cron diff --git a/packages/percolatestudio-synced-cron/example/.meteor/release b/packages/percolatestudio-synced-cron/example/.meteor/release new file mode 100755 index 00000000000..8f6f45d1740 --- /dev/null +++ b/packages/percolatestudio-synced-cron/example/.meteor/release @@ -0,0 +1 @@ +METEOR@0.9.1.1 diff --git a/packages/percolatestudio-synced-cron/example/.meteor/versions b/packages/percolatestudio-synced-cron/example/.meteor/versions new file mode 100755 index 00000000000..89cf11cf19e --- /dev/null +++ b/packages/percolatestudio-synced-cron/example/.meteor/versions @@ -0,0 +1,46 @@ +application-configuration@1.0.1 +autopublish@1.0.0 +autoupdate@1.0.6 +binary-heap@1.0.0 +blaze-tools@1.0.0 +blaze@2.0.0 +callback-hook@1.0.0 +check@1.0.0 +ctl-helper@1.0.3 +ctl@1.0.1 +ddp@1.0.8 +deps@1.0.3 +ejson@1.0.1 +follower-livedata@1.0.1 +geojson-utils@1.0.0 +html-tools@1.0.0 +htmljs@1.0.0 +id-map@1.0.0 +insecure@1.0.0 +jquery@1.0.0 +json@1.0.0 +livedata@1.0.9 +logging@1.0.2 +meteor-platform@1.0.2 +meteor@1.0.3 +minifiers@1.0.2 +minimongo@1.0.2 +mongo@1.0.4 +observe-sequence@1.0.2 +ordered-dict@1.0.0 +percolate:synced-cron@1.2.0 +random@1.0.0 +reactive-dict@1.0.2 +reactive-var@1.0.1 +reload@1.0.1 +retry@1.0.0 +routepolicy@1.0.0 +session@1.0.1 +spacebars-compiler@1.0.2 +spacebars@1.0.1 +standard-app-packages@1.0.1 +templating@1.0.5 +tracker@1.0.2 +ui@1.0.2 +underscore@1.0.0 +webapp@1.0.3 diff --git a/packages/percolatestudio-synced-cron/example/example.html b/packages/percolatestudio-synced-cron/example/example.html new file mode 100755 index 00000000000..3f05c834477 --- /dev/null +++ b/packages/percolatestudio-synced-cron/example/example.html @@ -0,0 +1,13 @@ + + example + + + + {{> hello}} + + + diff --git a/packages/percolatestudio-synced-cron/example/example.js b/packages/percolatestudio-synced-cron/example/example.js new file mode 100755 index 00000000000..9f4a86d1b92 --- /dev/null +++ b/packages/percolatestudio-synced-cron/example/example.js @@ -0,0 +1,41 @@ +if (Meteor.isClient) { + Template.hello.greeting = function () { + return "Welcome to example."; + }; + + Template.hello.events({ + 'click input': function () { + // template data, if any, is available in 'this' + if (typeof console !== 'undefined') + console.log("You pressed the button"); + } + }); +} + +if (Meteor.isServer) { + // optionally set the collection's name that synced cron will use + SyncedCron.config({ + collectionName: 'somethingDifferent' + }); + + SyncedCron.add({ + name: 'Crunch some important numbers for the marketing department', + schedule: function(parser) { + // parser is a later.parse object + return parser.text('every 5 seconds'); + }, + job: function(intendedAt) { + console.log('crunching numbers'); + console.log('job should be running at:'); + console.log(intendedAt); + } + }); + + Meteor.startup(function () { + // code to run on server at startup + SyncedCron.start(); + + // Stop jobs after 15 seconds + Meteor.setTimeout(function() { SyncedCron.stop(); }, 15 * 1000); + }); +} diff --git a/packages/percolatestudio-synced-cron/example/packages/synced-cron b/packages/percolatestudio-synced-cron/example/packages/synced-cron new file mode 120000 index 00000000000..c25bddb6dd4 --- /dev/null +++ b/packages/percolatestudio-synced-cron/example/packages/synced-cron @@ -0,0 +1 @@ +../.. \ No newline at end of file diff --git a/packages/percolatestudio-synced-cron/package.js b/packages/percolatestudio-synced-cron/package.js new file mode 100755 index 00000000000..5b51477129e --- /dev/null +++ b/packages/percolatestudio-synced-cron/package.js @@ -0,0 +1,20 @@ +Package.describe({ + summary: "Allows you to define and run scheduled jobs across multiple servers.", + version: "1.2.1", + name: "percolate:synced-cron", + git: "https://github.com/percolatestudio/meteor-synced-cron.git" +}); + +Npm.depends({later: "1.1.6"}); + +Package.onUse(function (api) { + api.versionsFrom('METEOR@0.9.1.1'); + api.use(['underscore', 'check', 'mongo', 'logging'], 'server'); + api.add_files(['synced-cron-server.js'], "server"); + api.export('SyncedCron', 'server'); +}); + +Package.onTest(function (api) { + api.use(['percolate:synced-cron', 'tinytest']); + api.add_files('synced-cron-tests.js', ['server']); +}); diff --git a/packages/percolatestudio-synced-cron/synced-cron-server.js b/packages/percolatestudio-synced-cron/synced-cron-server.js new file mode 100755 index 00000000000..15c3a8490da --- /dev/null +++ b/packages/percolatestudio-synced-cron/synced-cron-server.js @@ -0,0 +1,325 @@ +// A package for running jobs synchronized across multiple processes +SyncedCron = { + _entries: {}, + running: false, + options: { + //Log job run details to console + log: true, + + logger: null, + + //Name of collection to use for synchronisation and logging + collectionName: 'cronHistory', + + //Default to using localTime + utc: false, + + //TTL in seconds for history records in collection to expire + //NOTE: Unset to remove expiry but ensure you remove the index from + //mongo by hand + collectionTTL: 172800 + }, + config: function(opts) { + this.options = _.extend({}, this.options, opts); + } +} + +Later = Npm.require('later'); + +/* + Logger factory function. Takes a prefix string and options object + and uses an injected `logger` if provided, else falls back to + Meteor's `Log` package. + + Will send a log object to the injected logger, on the following form: + + message: String + level: String (info, warn, error, debug) + tag: 'SyncedCron' +*/ +function createLogger(prefix) { + check(prefix, String); + + // Return noop if logging is disabled. + if(SyncedCron.options.log === false) { + return function() {}; + } + + return function(level, message) { + check(level, Match.OneOf('info', 'error', 'warn', 'debug')); + check(message, String); + + var logger = SyncedCron.options && SyncedCron.options.logger; + + if(logger && _.isFunction(logger)) { + + logger({ + level: level, + message: message, + tag: prefix + }); + + } else { + Log[level]({ message: prefix + ': ' + message }); + } + } +} + +var log; + +Meteor.startup(function() { + var options = SyncedCron.options; + + log = createLogger('SyncedCron'); + + ['info', 'warn', 'error', 'debug'].forEach(function(level) { + log[level] = _.partial(log, level); + }); + + // Don't allow TTL less than 5 minutes so we don't break synchronization + var minTTL = 300; + + // Use UTC or localtime for evaluating schedules + if (options.utc) + Later.date.UTC(); + else + Later.date.localTime(); + + // collection holding the job history records + SyncedCron._collection = new Mongo.Collection(options.collectionName); + SyncedCron._collection._ensureIndex({intendedAt: 1, name: 1}, {unique: true}); + + if (options.collectionTTL) { + if (options.collectionTTL > minTTL) + SyncedCron._collection._ensureIndex({startedAt: 1 }, + { expireAfterSeconds: options.collectionTTL } ); + else + log.warn('Not going to use a TTL that is shorter than:' + minTTL); + } +}); + +var scheduleEntry = function(entry) { + var schedule = entry.schedule(Later.parse); + entry._timer = + SyncedCron._laterSetInterval(SyncedCron._entryWrapper(entry), schedule); + + log.info('Scheduled "' + entry.name + '" next run @' + + Later.schedule(schedule).next(1)); +} + +// add a scheduled job +// SyncedCron.add({ +// name: String, //*required* unique name of the job +// schedule: function(laterParser) {},//*required* when to run the job +// job: function() {}, //*required* the code to run +// }); +SyncedCron.add = function(entry) { + check(entry.name, String); + check(entry.schedule, Function); + check(entry.job, Function); + + // check + if (!this._entries[entry.name]) { + this._entries[entry.name] = entry; + + // If cron is already running, start directly. + if (this.running) { + scheduleEntry(entry); + } + } +} + +// Start processing added jobs +SyncedCron.start = function() { + var self = this; + + Meteor.startup(function() { + // Schedule each job with later.js + _.each(self._entries, function(entry) { + scheduleEntry(entry); + }); + self.running = true; + }); +} + +// Return the next scheduled date of the first matching entry or undefined +SyncedCron.nextScheduledAtDate = function(jobName) { + var entry = this._entries[jobName]; + + if (entry) + return Later.schedule(entry.schedule(Later.parse)).next(1); +} + +// Remove and stop the entry referenced by jobName +SyncedCron.remove = function(jobName) { + var entry = this._entries[jobName]; + + if (entry) { + if (entry._timer) + entry._timer.clear(); + + delete this._entries[jobName]; + log.info('Removed "' + entry.name); + } +} + +// Pause processing, but do not remove jobs so that the start method will +// restart existing jobs +SyncedCron.pause = function() { + if (this.running) { + _.each(this._entries, function(entry) { + entry._timer.clear(); + }); + this.running = false; + } +} + +// Stop processing and remove ALL jobs +SyncedCron.stop = function() { + _.each(this._entries, function(entry, name) { + SyncedCron.remove(name); + }); + this.running = false; +} + +// The meat of our logic. Checks if the specified has already run. If not, +// records that it's running the job, runs it, and records the output +SyncedCron._entryWrapper = function(entry) { + var self = this; + + return function(intendedAt) { + var jobHistory = { + intendedAt: intendedAt, + name: entry.name, + startedAt: new Date() + }; + + // If we have a dup key error, another instance has already tried to run + // this job. + try { + jobHistory._id = self._collection.insert(jobHistory); + } catch(e) { + // http://www.mongodb.org/about/contributors/error-codes/ + // 11000 == duplicate key error + if (e.name === 'MongoError' && e.code === 11000) { + log.info('Not running "' + entry.name + '" again.'); + return; + } + + throw e; + }; + + // run and record the job + try { + log.info('Starting "' + entry.name + '".'); + var output = entry.job(intendedAt); // <- Run the actual job + + log.info('Finished "' + entry.name + '".'); + self._collection.update({_id: jobHistory._id}, { + $set: { + finishedAt: new Date(), + result: output + } + }); + } catch(e) { + log.info('Exception "' + entry.name +'" ' + e.stack); + self._collection.update({_id: jobHistory._id}, { + $set: { + finishedAt: new Date(), + error: e.stack + } + }); + } + }; +} + +// for tests +SyncedCron._reset = function() { + this._entries = {}; + this._collection.remove({}); + this.running = false; +} + +// --------------------------------------------------------------------------- +// The following two functions are lifted from the later.js package, however +// I've made the following changes: +// - Use Meteor.setTimeout and Meteor.clearTimeout +// - Added an 'intendedAt' parameter to the callback fn that specifies the precise +// time the callback function *should* be run (so we can co-ordinate jobs) +// between multiple, potentially laggy and unsynced machines + +// From: https://github.com/bunkat/later/blob/master/src/core/setinterval.js +SyncedCron._laterSetInterval = function(fn, sched) { + + var t = SyncedCron._laterSetTimeout(scheduleTimeout, sched), + done = false; + + /** + * Executes the specified function and then sets the timeout for the next + * interval. + */ + function scheduleTimeout(intendedAt) { + if(!done) { + fn(intendedAt); + t = SyncedCron._laterSetTimeout(scheduleTimeout, sched); + } + } + + return { + + /** + * Clears the timeout. + */ + clear: function() { + done = true; + t.clear(); + } + + }; + +}; + +// From: https://github.com/bunkat/later/blob/master/src/core/settimeout.js +SyncedCron._laterSetTimeout = function(fn, sched) { + + var s = Later.schedule(sched), t; + scheduleTimeout(); + + /** + * Schedules the timeout to occur. If the next occurrence is greater than the + * max supported delay (2147483647 ms) than we delay for that amount before + * attempting to schedule the timeout again. + */ + function scheduleTimeout() { + var now = Date.now(), + next = s.next(2, now), + diff = next[0].getTime() - now, + intendedAt = next[0]; + + // minimum time to fire is one second, use next occurrence instead + if(diff < 1000) { + diff = next[1].getTime() - now; + intendedAt = next[1]; + } + + if(diff < 2147483647) { + t = Meteor.setTimeout(function() { fn(intendedAt); }, diff); + } + else { + t = Meteor.setTimeout(scheduleTimeout, 2147483647); + } + } + + return { + + /** + * Clears the timeout. + */ + clear: function() { + Meteor.clearTimeout(t); + } + + }; + +}; +// --------------------------------------------------------------------------- diff --git a/packages/percolatestudio-synced-cron/synced-cron-tests.js b/packages/percolatestudio-synced-cron/synced-cron-tests.js new file mode 100755 index 00000000000..78101b34d6a --- /dev/null +++ b/packages/percolatestudio-synced-cron/synced-cron-tests.js @@ -0,0 +1,216 @@ +Later = Npm.require('later'); + +Later.date.localTime(); // corresponds to SyncedCron.options.utc: true; + +var TestEntry = { + name: 'Test Job', + schedule: function(parser) { + return parser.cron('15 10 * * ? *'); // not required + }, + job: function() { + return 'ran'; + } +}; + +Tinytest.add('Syncing works', function(test) { + SyncedCron._reset(); + test.equal(SyncedCron._collection.find().count(), 0); + + // added the entry ok + SyncedCron.add(TestEntry); + test.equal(_.keys(SyncedCron._entries).length, 1); + + var entry = SyncedCron._entries[TestEntry.name]; + var intendedAt = new Date(); //whatever + + // first run + SyncedCron._entryWrapper(entry)(intendedAt); + test.equal(SyncedCron._collection.find().count(), 1); + var jobHistory1 = SyncedCron._collection.findOne(); + test.equal(jobHistory1.result, 'ran'); + + // second run + SyncedCron._entryWrapper(entry)(intendedAt); + test.equal(SyncedCron._collection.find().count(), 1); // should still be 1 + var jobHistory2 = SyncedCron._collection.findOne(); + test.equal(jobHistory1._id, jobHistory2._id); +}); + +Tinytest.add('Exceptions work', function(test) { + SyncedCron._reset(); + SyncedCron.add(_.extend({}, TestEntry, { + job: function() { + throw new Meteor.Error('Haha, gotcha!'); + } + }) + ); + + var entry = SyncedCron._entries[TestEntry.name]; + var intendedAt = new Date(); //whatever + + // error without result + SyncedCron._entryWrapper(entry)(intendedAt); + test.equal(SyncedCron._collection.find().count(), 1); + var jobHistory1 = SyncedCron._collection.findOne(); + test.equal(jobHistory1.result, undefined); + test.matches(jobHistory1.error, /Haha, gotcha/); +}); + +Tinytest.add('SyncedCron.nextScheduledAtDate works', function(test) { + SyncedCron._reset(); + test.equal(SyncedCron._collection.find().count(), 0); + + // addd 2 entries + SyncedCron.add(TestEntry); + + var entry2 = _.extend({}, TestEntry, { + name: 'Test Job2', + schedule: function(parser) { + return parser.cron('30 11 * * ? *'); + } + }); + SyncedCron.add(entry2); + + test.equal(_.keys(SyncedCron._entries).length, 2); + + SyncedCron.start(); + + var date = SyncedCron.nextScheduledAtDate(entry2.name); + var correctDate = Later.schedule(entry2.schedule(Later.parse)).next(1); + + test.equal(date, correctDate); +}); + +// Tests SyncedCron.remove in the process +Tinytest.add('SyncedCron.stop works', function(test) { + SyncedCron._reset(); + test.equal(SyncedCron._collection.find().count(), 0); + + // addd 2 entries + SyncedCron.add(TestEntry); + + var entry2 = _.extend({}, TestEntry, { + name: 'Test Job2', + schedule: function(parser) { + return parser.cron('30 11 * * ? *'); + } + }); + SyncedCron.add(entry2); + + SyncedCron.start(); + + test.equal(_.keys(SyncedCron._entries).length, 2); + + SyncedCron.stop(); + + test.equal(_.keys(SyncedCron._entries).length, 0); +}); + +Tinytest.add('SyncedCron.pause works', function(test) { + SyncedCron._reset(); + test.equal(SyncedCron._collection.find().count(), 0); + + // addd 2 entries + SyncedCron.add(TestEntry); + + var entry2 = _.extend({}, TestEntry, { + name: 'Test Job2', + schedule: function(parser) { + return parser.cron('30 11 * * ? *'); + } + }); + SyncedCron.add(entry2); + + SyncedCron.start(); + + test.equal(_.keys(SyncedCron._entries).length, 2); + + SyncedCron.pause(); + + test.equal(_.keys(SyncedCron._entries).length, 2); + test.isFalse(SyncedCron.running); + + SyncedCron.start(); + + test.equal(_.keys(SyncedCron._entries).length, 2); + test.isTrue(SyncedCron.running); + +}); + +// Tests SyncedCron.remove in the process +Tinytest.add('SyncedCron.add starts by it self when running', function(test) { + SyncedCron._reset(); + + test.equal(SyncedCron._collection.find().count(), 0); + test.equal(SyncedCron.running, false); + Log._intercept(2); + + SyncedCron.start(); + + test.equal(SyncedCron.running, true); + + // addd 1 entries + SyncedCron.add(TestEntry); + + test.equal(_.keys(SyncedCron._entries).length, 1); + + SyncedCron.stop(); + + var intercepted = Log._intercepted(); + test.equal(intercepted.length, 2); + + test.equal(SyncedCron.running, false); + test.equal(_.keys(SyncedCron._entries).length, 0); +}); + +Tinytest.add('SyncedCron.config can customize the options object', function(test) { + SyncedCron._reset(); + + SyncedCron.config({ + log: false, + collectionName: 'foo', + utc: true, + collectionTTL: 0 + }); + + test.equal(SyncedCron.options.log, false); + test.equal(SyncedCron.options.collectionName, 'foo'); + test.equal(SyncedCron.options.utc, true); + test.equal(SyncedCron.options.collectionTTL, 0); +}); + +Tinytest.addAsync('SyncedCron can log to injected logger', function(test, done) { + SyncedCron._reset(); + + var logger = function() { + test.isTrue(true); + done(); + }; + + SyncedCron.options.logger = logger; + + SyncedCron.add(TestEntry); + SyncedCron.start(); + + SyncedCron.options.logger = null; +}); + +Tinytest.addAsync('SyncedCron should pass correct arguments to logger', function(test, done) { + SyncedCron._reset(); + + var logger = function(opts) { + test.include(opts, 'level'); + test.include(opts, 'message'); + test.include(opts, 'tag'); + test.equal(opts.tag, 'SyncedCron'); + + done(); + }; + + SyncedCron.options.logger = logger; + + SyncedCron.add(TestEntry); + SyncedCron.start(); + + SyncedCron.options.logger = null; +}); diff --git a/packages/percolatestudio-synced-cron/versions.json b/packages/percolatestudio-synced-cron/versions.json new file mode 100755 index 00000000000..5711dabf625 --- /dev/null +++ b/packages/percolatestudio-synced-cron/versions.json @@ -0,0 +1,83 @@ +{ + "dependencies": [ + [ + "application-configuration", + "1.0.1" + ], + [ + "binary-heap", + "1.0.0" + ], + [ + "callback-hook", + "1.0.0" + ], + [ + "check", + "1.0.0" + ], + [ + "ddp", + "1.0.8" + ], + [ + "ejson", + "1.0.1" + ], + [ + "follower-livedata", + "1.0.1" + ], + [ + "geojson-utils", + "1.0.0" + ], + [ + "id-map", + "1.0.0" + ], + [ + "json", + "1.0.0" + ], + [ + "logging", + "1.0.2" + ], + [ + "meteor", + "1.0.3" + ], + [ + "minimongo", + "1.0.2" + ], + [ + "mongo", + "1.0.4" + ], + [ + "ordered-dict", + "1.0.0" + ], + [ + "random", + "1.0.0" + ], + [ + "retry", + "1.0.0" + ], + [ + "tracker", + "1.0.2" + ], + [ + "underscore", + "1.0.0" + ] + ], + "pluginDependencies": [], + "toolVersion": "meteor-tool@1.0.28", + "format": "1.0" +} \ No newline at end of file diff --git a/server/startup/cron.coffee b/server/startup/cron.coffee index ca676d5cec6..7fc8192ce17 100644 --- a/server/startup/cron.coffee +++ b/server/startup/cron.coffee @@ -1,17 +1,16 @@ +# Config and Start SyncedCron +SyncedCron.config + collectionName: 'rocketchat_cron_history' + +# Generate and save statistics every hour +SyncedCron.add + name: 'Generate and save statistics every hour', + schedule: (parser) -># parser is a later.parse object + return parser.text 'every 1 minute' + job: -> + statistics = RocketChat.saveStatistics() + return + Meteor.startup -> Meteor.defer -> - - # Config and Start SyncedCron - SyncedCron.config - collectionName: 'rocketchat_cron_history' - - # Generate and save statistics every hour - SyncedCron.add - name: 'Generate and save statistics every hour', - schedule: (parser) -># parser is a later.parse object - return parser.text 'every 1 hours' - job: -> - statistics = RocketChat.saveStatistics - return - SyncedCron.start() From 87f87bb344a78013a6b7795bdcef0653f7956ae4 Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Wed, 26 Aug 2015 23:40:01 -0300 Subject: [PATCH 3/8] Sending statistics to web site --- .../settings/server/startup.coffee | 2 ++ server/startup/cron.coffee | 32 +++++++++++++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/rocketchat-lib/settings/server/startup.coffee b/packages/rocketchat-lib/settings/server/startup.coffee index 6e8dd9bff39..4c94b9380bd 100644 --- a/packages/rocketchat-lib/settings/server/startup.coffee +++ b/packages/rocketchat-lib/settings/server/startup.coffee @@ -73,6 +73,8 @@ Meteor.startup -> RocketChat.settings.add 'Layout_Privacy_Policy', 'Privacy Policy
Go to APP SETTINGS -> Layout to customize this page.', { type: 'string', multiline: true, group: 'Layout', public: true } RocketChat.settings.add 'Layout_Sidenav_Footer', '', { type: 'string', group: 'Layout', public: true, i18nDescription: 'Layout_Sidenav_Footer_description' } + RocketChat.settings.add 'Statistics_opt_out', false, { type: 'boolean', group: false } + if process?.env? and not process.env['MAIL_URL']? and RocketChat.settings.get('SMTP_Host') and RocketChat.settings.get('SMTP_Username') and RocketChat.settings.get('SMTP_Password') process.env['MAIL_URL'] = "smtp://" + encodeURIComponent(RocketChat.settings.get('SMTP_Username')) + ':' + encodeURIComponent(RocketChat.settings.get('SMTP_Password')) + '@' + encodeURIComponent(RocketChat.settings.get('SMTP_Host')) if RocketChat.settings.get('SMTP_Port') diff --git a/server/startup/cron.coffee b/server/startup/cron.coffee index 7fc8192ce17..0ef3a1f6eaf 100644 --- a/server/startup/cron.coffee +++ b/server/startup/cron.coffee @@ -2,15 +2,29 @@ SyncedCron.config collectionName: 'rocketchat_cron_history' -# Generate and save statistics every hour -SyncedCron.add - name: 'Generate and save statistics every hour', - schedule: (parser) -># parser is a later.parse object - return parser.text 'every 1 minute' - job: -> - statistics = RocketChat.saveStatistics() - return - Meteor.startup -> Meteor.defer -> + + # Generate and save statistics every hour + SyncedCron.add + name: 'Generate and save statistics', + schedule: (parser) -># parser is a later.parse object + return parser.text 'every 1 hour' + job: -> + statistics = RocketChat.saveStatistics() + return + + unless RocketChat.settings.get 'Statistics_opt_out' + # Generate and save statistics every hour + SyncedCron.add + name: 'Send statistics to Rocket.Chat', + schedule: (parser) -># parser is a later.parse object + return parser.text 'every 1 day' + job: -> + statistics = RocketChat.getAvgStatistics() + # HTTP.post 'http://localhost:3005/stats', + HTTP.post 'https://rocket.chat/stats', + data: statistics + return + SyncedCron.start() From ecebbdbe64e0313c82eebbc3a41480e257744856 Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Thu, 27 Aug 2015 23:04:59 -0300 Subject: [PATCH 4/8] Opt out statistics; --- .meteor/versions | 2 +- client/views/admin/adminStatistics.coffee | 18 ++++++++++++++++++ client/views/admin/adminStatistics.html | 6 ++++++ i18n/en.i18n.json | 2 ++ .../functions/checkUsernameAvailability.coffee | 2 +- .../server/functions/getStatistics.coffee | 1 + .../server/functions/setUsername.coffee | 2 +- server/startup/cron.coffee | 16 ++++------------ 8 files changed, 34 insertions(+), 15 deletions(-) diff --git a/.meteor/versions b/.meteor/versions index 12f8c33738e..73c0166c569 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -39,7 +39,7 @@ htmljs@1.0.4 http@1.1.0 id-map@1.0.3 idorecall:email-normalize@1.0.0 -jalik:ufs@0.2.7 +jalik:ufs@0.2.8 jalik:ufs-gridfs@0.1.0 jparker:crypto-core@0.1.0 jparker:crypto-md5@0.1.1 diff --git a/client/views/admin/adminStatistics.coffee b/client/views/admin/adminStatistics.coffee index 454a0e16711..f0d629a7f78 100644 --- a/client/views/admin/adminStatistics.coffee +++ b/client/views/admin/adminStatistics.coffee @@ -26,12 +26,30 @@ Template.adminStatistics.helpers return out numFormat: (number) -> return _.numberFormat(number, 2) + optOut: -> + return RocketChat.settings.get 'Statistics_opt_out' + +Template.adminStatistics.events + 'click input[name=opt-out-statistics]': (e) -> + if $(e.currentTarget).prop('checked') + $('#opt-out-warning').show() + RocketChat.settings.set 'Statistics_opt_out', true, -> + toastr.success TAPi18next.t 'project:Settings_updated' + else + $('#opt-out-warning').hide() + RocketChat.settings.set 'Statistics_opt_out', false, -> + toastr.success TAPi18next.t 'project:Settings_updated' Template.adminStatistics.onRendered -> Tracker.afterFlush -> SideNav.setFlex "adminFlex" SideNav.openFlex() + if RocketChat.settings.get 'Statistics_opt_out' + $('#opt-out-warning').show() + else + $('#opt-out-warning').hide() + Template.adminStatistics.onCreated -> instance = @ @statistics = new ReactiveVar {} diff --git a/client/views/admin/adminStatistics.html b/client/views/admin/adminStatistics.html index 7a6d8b0ecb2..fd02286c166 100644 --- a/client/views/admin/adminStatistics.html +++ b/client/views/admin/adminStatistics.html @@ -109,6 +109,12 @@ {{_ "Please_wait_statistics"}} {{/if}} {{/unless}} +
+ +
+ {{_ "Opt_out_statistics_warning"}} +
+
\ No newline at end of file diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 493b0248894..4ac0e4a4a69 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -168,6 +168,8 @@ "Notify_all_in_this_room" : "Notify all in this room", "Online" : "Online", "Oops!" : "Oops", + "Opt_out_statistics": "Don't send my anonymous statistics to Rocket.Chat", + "Opt_out_statistics_warning": "By sending your anonymous statistics, you'll help us identify how many instances of Rocket.Chat are deployed, as well as how good the system is behaving, so we can further improve it. If you want to continue sending us your anonymous statistics, uncheck the above checkbox. Thank you.", "others" : "others", "Password" : "Password", "Password_changed_successfully" : "Password changed successfully", diff --git a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee index 1dd34b347b5..ea855eb7ecc 100644 --- a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee +++ b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee @@ -1,2 +1,2 @@ RocketChat.checkUsernameAvailability = (username) -> - return not Meteor.users.findOne({ username: { $regex : new RegExp("^" + _.trim(username) + "$", "i") } }) \ No newline at end of file + return not Meteor.users.findOne({ username: { $regex : new RegExp("^" + s.trim(username) + "$", "i") } }) \ No newline at end of file diff --git a/packages/rocketchat-lib/server/functions/getStatistics.coffee b/packages/rocketchat-lib/server/functions/getStatistics.coffee index 07a35c9b276..0dbd632487e 100644 --- a/packages/rocketchat-lib/server/functions/getStatistics.coffee +++ b/packages/rocketchat-lib/server/functions/getStatistics.coffee @@ -2,6 +2,7 @@ RocketChat.getStatistics = -> statistics = {} # Version + statistics.uniqueId = Settings.findOne({ _id: "uniqueID" })?.value statistics.version = BuildInfo?.commit?.hash statistics.versionDate = BuildInfo?.commit?.date diff --git a/packages/rocketchat-lib/server/functions/setUsername.coffee b/packages/rocketchat-lib/server/functions/setUsername.coffee index b16764b61fa..aed65e1e30a 100644 --- a/packages/rocketchat-lib/server/functions/setUsername.coffee +++ b/packages/rocketchat-lib/server/functions/setUsername.coffee @@ -1,5 +1,5 @@ RocketChat.setUsername = (user, username) -> - username = _.trim username + username = s.trim username if not user or not username return false diff --git a/server/startup/cron.coffee b/server/startup/cron.coffee index 0ef3a1f6eaf..0ec5602289e 100644 --- a/server/startup/cron.coffee +++ b/server/startup/cron.coffee @@ -12,19 +12,11 @@ Meteor.startup -> return parser.text 'every 1 hour' job: -> statistics = RocketChat.saveStatistics() - return - - unless RocketChat.settings.get 'Statistics_opt_out' - # Generate and save statistics every hour - SyncedCron.add - name: 'Send statistics to Rocket.Chat', - schedule: (parser) -># parser is a later.parse object - return parser.text 'every 1 day' - job: -> - statistics = RocketChat.getAvgStatistics() - # HTTP.post 'http://localhost:3005/stats', + unless RocketChat.settings.get 'Statistics_opt_out' + console.log 'Sending statistics data to Rocket.Chat' HTTP.post 'https://rocket.chat/stats', data: statistics - return + + return SyncedCron.start() From 14b2477d0777e5f5d956f14107202b5f39724593 Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Fri, 28 Aug 2015 18:10:52 -0300 Subject: [PATCH 5/8] Initial development of statistics package --- .meteor/packages | 1 + .meteor/versions | 1 + .../rocketchat-statistics/i18n/en.i18n.json | 3 ++ .../rocketchat-statistics/package-tap.i18n | 0 packages/rocketchat-statistics/package.js | 32 +++++++++++++++++++ .../server/Statistics.coffee | 1 + server/lib/collections.coffee | 1 - 7 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 packages/rocketchat-statistics/i18n/en.i18n.json create mode 100644 packages/rocketchat-statistics/package-tap.i18n create mode 100644 packages/rocketchat-statistics/package.js create mode 100644 packages/rocketchat-statistics/server/Statistics.coffee diff --git a/.meteor/packages b/.meteor/packages index 0d513e0252d..37688ae9846 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -81,3 +81,4 @@ monbro:mongodb-mapreduce-aggregation accounts-gitlab gitlab rocketchat:gitlab +rocketchat:statistics diff --git a/.meteor/versions b/.meteor/versions index 0a7f4470b7c..ce37a55842c 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -116,6 +116,7 @@ rocketchat:markdown@0.0.1 rocketchat:me@0.0.1 rocketchat:mentions@0.0.1 rocketchat:oembed@0.0.1 +rocketchat:statistics@0.0.1 rocketchat:webrtc@0.0.1 routepolicy@1.0.5 service-configuration@1.0.4 diff --git a/packages/rocketchat-statistics/i18n/en.i18n.json b/packages/rocketchat-statistics/i18n/en.i18n.json new file mode 100644 index 00000000000..4eb7c3f9836 --- /dev/null +++ b/packages/rocketchat-statistics/i18n/en.i18n.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/packages/rocketchat-statistics/package-tap.i18n b/packages/rocketchat-statistics/package-tap.i18n new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/rocketchat-statistics/package.js b/packages/rocketchat-statistics/package.js new file mode 100644 index 00000000000..16486b2ed2c --- /dev/null +++ b/packages/rocketchat-statistics/package.js @@ -0,0 +1,32 @@ +Package.describe({ + name: 'rocketchat:statistics', + version: '0.0.1', + summary: 'Statistics generator', + git: '' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.0'); + + api.use([ + 'templating', + 'coffeescript', + 'rocketchat:lib@0.0.1' + ]); + + // TAPi18n + api.use(["tap:i18n@1.5.1"], ["client", "server"]); + api.addFiles("package-tap.i18n", ["client", "server"]); + api.addFiles([ + "i18n/en.i18n.json", + ], ["client", "server"]); + + // Statistics + api.addFiles('server/Statistics.coffee', 'server'); + + api.export('Statistics', 'server'); +}); + +Package.onTest(function(api) { + +}); diff --git a/packages/rocketchat-statistics/server/Statistics.coffee b/packages/rocketchat-statistics/server/Statistics.coffee new file mode 100644 index 00000000000..3cf64d0319c --- /dev/null +++ b/packages/rocketchat-statistics/server/Statistics.coffee @@ -0,0 +1 @@ +@Statistics = new Meteor.Collection 'rocketchat_statistics' diff --git a/server/lib/collections.coffee b/server/lib/collections.coffee index 4c22f223ad0..cf46a682a08 100644 --- a/server/lib/collections.coffee +++ b/server/lib/collections.coffee @@ -2,5 +2,4 @@ @ChatRoom = new Meteor.Collection 'rocketchat_room' @ChatSubscription = new Meteor.Collection 'rocketchat_subscription' @MapReducedStatistics = new Mongo.Collection 'rocketchat_mr_statistics' -@Statistics = new Mongo.Collection 'rocketchat_statistics' @ChatReports = new Meteor.Collection 'rocketchat_reports' From 5954a7905fd10322695eaba6abf1acd76e79716e Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Sun, 30 Aug 2015 21:41:50 -0300 Subject: [PATCH 6/8] Moved statistics to package --- packages/rocketchat-lib/package.js | 4 ---- .../server/functions/saveStatistics.coffee | 5 ----- .../server/methods/generateStatistics.coffee | 11 ----------- packages/rocketchat-statistics/lib/rocketchat.coffee | 1 + packages/rocketchat-statistics/package.js | 8 ++++++-- .../server/collections/MapReducedStatistics.coffee | 1 + .../server/{ => collections}/Statistics.coffee | 0 .../server/functions/get.coffee} | 2 +- .../server/functions/getAverage.coffee} | 2 +- .../server/functions/save.coffee | 6 ++++++ .../server/methods/getStatistics.coffee | 11 +++++++++++ 11 files changed, 27 insertions(+), 24 deletions(-) delete mode 100644 packages/rocketchat-lib/server/functions/saveStatistics.coffee delete mode 100644 packages/rocketchat-lib/server/methods/generateStatistics.coffee create mode 100644 packages/rocketchat-statistics/lib/rocketchat.coffee create mode 100644 packages/rocketchat-statistics/server/collections/MapReducedStatistics.coffee rename packages/rocketchat-statistics/server/{ => collections}/Statistics.coffee (100%) rename packages/{rocketchat-lib/server/functions/getStatistics.coffee => rocketchat-statistics/server/functions/get.coffee} (97%) rename packages/{rocketchat-lib/server/functions/getAvgStatistics.coffee => rocketchat-statistics/server/functions/getAverage.coffee} (97%) create mode 100644 packages/rocketchat-statistics/server/functions/save.coffee create mode 100644 packages/rocketchat-statistics/server/methods/getStatistics.coffee diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 2d0067656d1..6cedf043ed4 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -20,14 +20,10 @@ Package.onUse(function(api) { api.addFiles([ 'server/functions/checkUsernameAvailability.coffee', - 'server/functions/getAvgStatistics.coffee', - 'server/functions/getStatistics.coffee', - 'server/functions/saveStatistics.coffee', 'server/functions/setUsername.coffee' ], ['server']); api.addFiles([ - 'server/methods/generateStatistics.coffee', 'server/methods/joinDefaultChannels.coffee', 'server/methods/setAdminStatus.coffee', 'server/methods/setUsername.coffee', diff --git a/packages/rocketchat-lib/server/functions/saveStatistics.coffee b/packages/rocketchat-lib/server/functions/saveStatistics.coffee deleted file mode 100644 index fca2fb1c808..00000000000 --- a/packages/rocketchat-lib/server/functions/saveStatistics.coffee +++ /dev/null @@ -1,5 +0,0 @@ -RocketChat.saveStatistics = -> - statistics = RocketChat.getStatistics() - statistics.createdAt = new Date - Statistics.insert statistics - return statistics \ No newline at end of file diff --git a/packages/rocketchat-lib/server/methods/generateStatistics.coffee b/packages/rocketchat-lib/server/methods/generateStatistics.coffee deleted file mode 100644 index 2f6174ea08a..00000000000 --- a/packages/rocketchat-lib/server/methods/generateStatistics.coffee +++ /dev/null @@ -1,11 +0,0 @@ -Meteor.methods - generateStatistics: -> - if not Meteor.userId() - throw new Meteor.Error('invalid-user', "[methods] generateStatistics -> Invalid user") - - console.log '[methods] generateStatistics -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments - - unless Meteor.user()?.admin is true - throw new Meteor.Error 'not-authorized', '[methods] setAdminStatus -> Not authorized' - - return RocketChat.getStatistics() \ No newline at end of file diff --git a/packages/rocketchat-statistics/lib/rocketchat.coffee b/packages/rocketchat-statistics/lib/rocketchat.coffee new file mode 100644 index 00000000000..3111fcc9479 --- /dev/null +++ b/packages/rocketchat-statistics/lib/rocketchat.coffee @@ -0,0 +1 @@ +RocketChat.statistics = {} diff --git a/packages/rocketchat-statistics/package.js b/packages/rocketchat-statistics/package.js index 16486b2ed2c..541d7a82532 100644 --- a/packages/rocketchat-statistics/package.js +++ b/packages/rocketchat-statistics/package.js @@ -22,9 +22,13 @@ Package.onUse(function(api) { ], ["client", "server"]); // Statistics - api.addFiles('server/Statistics.coffee', 'server'); + api.addFiles('lib/rocketchat.coffee', [ 'client', 'server' ]); + api.addFiles([ + 'server/collections/Statistics.coffee', + 'server/functions/get.coffee', + 'server/functions/save.coffee' + ], 'server'); - api.export('Statistics', 'server'); }); Package.onTest(function(api) { diff --git a/packages/rocketchat-statistics/server/collections/MapReducedStatistics.coffee b/packages/rocketchat-statistics/server/collections/MapReducedStatistics.coffee new file mode 100644 index 00000000000..be489dba370 --- /dev/null +++ b/packages/rocketchat-statistics/server/collections/MapReducedStatistics.coffee @@ -0,0 +1 @@ +@MapReducedStatistics = new Meteor.Collection 'rocketchat_mr_statistics' diff --git a/packages/rocketchat-statistics/server/Statistics.coffee b/packages/rocketchat-statistics/server/collections/Statistics.coffee similarity index 100% rename from packages/rocketchat-statistics/server/Statistics.coffee rename to packages/rocketchat-statistics/server/collections/Statistics.coffee diff --git a/packages/rocketchat-lib/server/functions/getStatistics.coffee b/packages/rocketchat-statistics/server/functions/get.coffee similarity index 97% rename from packages/rocketchat-lib/server/functions/getStatistics.coffee rename to packages/rocketchat-statistics/server/functions/get.coffee index cfb726ed408..6194caa6be9 100644 --- a/packages/rocketchat-lib/server/functions/getStatistics.coffee +++ b/packages/rocketchat-statistics/server/functions/get.coffee @@ -1,4 +1,4 @@ -RocketChat.getStatistics = -> +RocketChat.statistics.get = -> statistics = {} # Version diff --git a/packages/rocketchat-lib/server/functions/getAvgStatistics.coffee b/packages/rocketchat-statistics/server/functions/getAverage.coffee similarity index 97% rename from packages/rocketchat-lib/server/functions/getAvgStatistics.coffee rename to packages/rocketchat-statistics/server/functions/getAverage.coffee index b76754d3f36..0f922daab0b 100644 --- a/packages/rocketchat-lib/server/functions/getAvgStatistics.coffee +++ b/packages/rocketchat-statistics/server/functions/getAverage.coffee @@ -1,4 +1,4 @@ -RocketChat.getAvgStatistics = -> +RocketChat.statistics.getAverage = -> statistics = {} m = -> diff --git a/packages/rocketchat-statistics/server/functions/save.coffee b/packages/rocketchat-statistics/server/functions/save.coffee new file mode 100644 index 00000000000..ed19daf541e --- /dev/null +++ b/packages/rocketchat-statistics/server/functions/save.coffee @@ -0,0 +1,6 @@ +RocketChat.statistics.save = -> + statistics = RocketChat.statistics.get() + statistics.createdAt = new Date + Statistics.insert statistics + return statistics + diff --git a/packages/rocketchat-statistics/server/methods/getStatistics.coffee b/packages/rocketchat-statistics/server/methods/getStatistics.coffee new file mode 100644 index 00000000000..dde2b348a10 --- /dev/null +++ b/packages/rocketchat-statistics/server/methods/getStatistics.coffee @@ -0,0 +1,11 @@ +Meteor.methods + getStatistics: -> + if not Meteor.userId() + throw new Meteor.Error('invalid-user', "[methods] getStatistics -> Invalid user") + + console.log '[methods] getStatistics -> '.green, 'userId:', Meteor.userId(), 'arguments:', arguments + + unless Meteor.user()?.admin is true + throw new Meteor.Error 'not-authorized', '[methods] getStatistics -> Not authorized' + + return RocketChat.statistics.get() \ No newline at end of file From 66e8bc5aaabc14c5f0898478a533249b022ff57a Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Mon, 31 Aug 2015 13:13:58 -0300 Subject: [PATCH 7/8] Merged statistics / changed method name --- server/startup/cron.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/startup/cron.coffee b/server/startup/cron.coffee index 0ec5602289e..fb431b6bd39 100644 --- a/server/startup/cron.coffee +++ b/server/startup/cron.coffee @@ -11,7 +11,7 @@ Meteor.startup -> schedule: (parser) -># parser is a later.parse object return parser.text 'every 1 hour' job: -> - statistics = RocketChat.saveStatistics() + statistics = RocketChat.statistics.save() unless RocketChat.settings.get 'Statistics_opt_out' console.log 'Sending statistics data to Rocket.Chat' HTTP.post 'https://rocket.chat/stats', From c6a887395f486bc67142600d0bf1e0a4eafe5eef Mon Sep 17 00:00:00 2001 From: Marcelo Schmidt Date: Mon, 31 Aug 2015 13:36:12 -0300 Subject: [PATCH 8/8] Replaced percolatestudio:synced-cron for percolate:synced-cron; --- .meteor/versions | 6 +- .../percolatestudio-synced-cron/.gitignore | 1 - .../.npm/package/.gitignore | 1 - .../.npm/package/README | 7 - .../.npm/package/npm-shrinkwrap.json | 7 - .../percolatestudio-synced-cron/.versions | 21 -- packages/percolatestudio-synced-cron/LICENSE | 20 -- .../percolatestudio-synced-cron/README.md | 137 -------- .../example/.meteor/.finished-upgraders | 6 - .../example/.meteor/.gitignore | 1 - .../example/.meteor/.id | 7 - .../example/.meteor/packages | 9 - .../example/.meteor/release | 1 - .../example/.meteor/versions | 46 --- .../example/example.html | 13 - .../example/example.js | 41 --- .../example/packages/synced-cron | 1 - .../percolatestudio-synced-cron/package.js | 20 -- .../synced-cron-server.js | 325 ------------------ .../synced-cron-tests.js | 216 ------------ .../percolatestudio-synced-cron/versions.json | 83 ----- 21 files changed, 3 insertions(+), 966 deletions(-) delete mode 100755 packages/percolatestudio-synced-cron/.gitignore delete mode 100644 packages/percolatestudio-synced-cron/.npm/package/.gitignore delete mode 100644 packages/percolatestudio-synced-cron/.npm/package/README delete mode 100644 packages/percolatestudio-synced-cron/.npm/package/npm-shrinkwrap.json delete mode 100755 packages/percolatestudio-synced-cron/.versions delete mode 100755 packages/percolatestudio-synced-cron/LICENSE delete mode 100755 packages/percolatestudio-synced-cron/README.md delete mode 100755 packages/percolatestudio-synced-cron/example/.meteor/.finished-upgraders delete mode 100755 packages/percolatestudio-synced-cron/example/.meteor/.gitignore delete mode 100755 packages/percolatestudio-synced-cron/example/.meteor/.id delete mode 100755 packages/percolatestudio-synced-cron/example/.meteor/packages delete mode 100755 packages/percolatestudio-synced-cron/example/.meteor/release delete mode 100755 packages/percolatestudio-synced-cron/example/.meteor/versions delete mode 100755 packages/percolatestudio-synced-cron/example/example.html delete mode 100755 packages/percolatestudio-synced-cron/example/example.js delete mode 120000 packages/percolatestudio-synced-cron/example/packages/synced-cron delete mode 100755 packages/percolatestudio-synced-cron/package.js delete mode 100755 packages/percolatestudio-synced-cron/synced-cron-server.js delete mode 100755 packages/percolatestudio-synced-cron/synced-cron-tests.js delete mode 100755 packages/percolatestudio-synced-cron/versions.json diff --git a/.meteor/versions b/.meteor/versions index 6307eb5b26e..d1478ddb584 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -46,8 +46,8 @@ jparker:crypto-md5@0.1.1 jparker:gravatar@0.4.1 jquery@1.11.3_2 json@1.0.3 -kadira:blaze-layout@2.0.0 -kadira:flow-router@2.3.0 +kadira:blaze-layout@2.0.1 +kadira:flow-router@2.4.0 kevohagan:sweetalert@1.0.0 konecty:autolinker@1.0.2 konecty:change-case@2.3.0 @@ -65,7 +65,7 @@ matb33:collection-hooks@0.7.14 meteor@1.1.6 meteor-developer@1.1.3 meteor-platform@1.2.2 -meteorhacks:kadira@2.23.0 +meteorhacks:kadira@2.23.1 meteorhacks:meteorx@1.3.1 meteorspark:util@0.2.0 minifiers@1.1.5 diff --git a/packages/percolatestudio-synced-cron/.gitignore b/packages/percolatestudio-synced-cron/.gitignore deleted file mode 100755 index 677a6fc2637..00000000000 --- a/packages/percolatestudio-synced-cron/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.build* diff --git a/packages/percolatestudio-synced-cron/.npm/package/.gitignore b/packages/percolatestudio-synced-cron/.npm/package/.gitignore deleted file mode 100644 index 3c3629e647f..00000000000 --- a/packages/percolatestudio-synced-cron/.npm/package/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/packages/percolatestudio-synced-cron/.npm/package/README b/packages/percolatestudio-synced-cron/.npm/package/README deleted file mode 100644 index 3d492553a43..00000000000 --- a/packages/percolatestudio-synced-cron/.npm/package/README +++ /dev/null @@ -1,7 +0,0 @@ -This directory and the files immediately inside it are automatically generated -when you change this package's NPM dependencies. Commit the files in this -directory (npm-shrinkwrap.json, .gitignore, and this README) to source control -so that others run the same versions of sub-dependencies. - -You should NOT check in the node_modules directory that Meteor automatically -creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/percolatestudio-synced-cron/.npm/package/npm-shrinkwrap.json b/packages/percolatestudio-synced-cron/.npm/package/npm-shrinkwrap.json deleted file mode 100644 index c40d192c454..00000000000 --- a/packages/percolatestudio-synced-cron/.npm/package/npm-shrinkwrap.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "later": { - "version": "1.1.6" - } - } -} diff --git a/packages/percolatestudio-synced-cron/.versions b/packages/percolatestudio-synced-cron/.versions deleted file mode 100755 index c868d531e29..00000000000 --- a/packages/percolatestudio-synced-cron/.versions +++ /dev/null @@ -1,21 +0,0 @@ -base64@1.0.3 -binary-heap@1.0.3 -callback-hook@1.0.3 -check@1.0.5 -ddp@1.1.0 -ejson@1.0.6 -geojson-utils@1.0.3 -id-map@1.0.3 -json@1.0.3 -local-test:percolate:synced-cron@1.2.0 -logging@1.0.7 -meteor@1.1.5 -minimongo@1.0.7 -mongo@1.1.0 -ordered-dict@1.0.3 -percolate:synced-cron@1.2.0 -random@1.0.3 -retry@1.0.3 -tinytest@1.0.5 -tracker@1.0.6 -underscore@1.0.3 diff --git a/packages/percolatestudio-synced-cron/LICENSE b/packages/percolatestudio-synced-cron/LICENSE deleted file mode 100755 index 84859725814..00000000000 --- a/packages/percolatestudio-synced-cron/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Percolate Studio - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/percolatestudio-synced-cron/README.md b/packages/percolatestudio-synced-cron/README.md deleted file mode 100755 index e0cdb5c7564..00000000000 --- a/packages/percolatestudio-synced-cron/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# percolate:synced-cron - -A simple cron system for [Meteor](http://meteor.com). It supports syncronizing jobs between multiple processes. In other words, if you add a job that runs every hour and your deployment consists of multiple app servers, only one of the app servers will execute the job each time (whichever tries first). - -## Installation - -``` sh -$ meteor add percolate:synced-cron -``` - -## API - -### Basics - -To write a cron job, give it a unique name, a schedule an a function to run like below. SyncedCron uses the fantastic [later.js](http://bunkat.github.io/later/) library behind the scenes. A Later.js `parse` object is passed into the schedule call that gives you a huge amount of flexibility for scheduling your jobs, see the [documentation](http://bunkat.github.io/later/parsers.html#overview). - -``` js -SyncedCron.add({ - name: 'Crunch some important numbers for the marketing department', - schedule: function(parser) { - // parser is a later.parse object - return parser.text('every 2 hours'); - }, - job: function() { - var numbersCrunched = CrushSomeNumbers(); - return numbersCrunched; - } -}); -``` - -To start processing your jobs, somewhere in your project add: - -``` js -SyncedCron.start(); -``` - -### Advanced - -SyncedCron uses a collection called `cronHistory` to syncronize between processes. This also serves as a useful log of when jobs ran along with their output or error. A sample item looks like: - -``` js -{ _id: 'wdYLPBZp5zzbwdfYj', - intendedAt: Sun Apr 13 2014 17:34:00 GMT-0700 (MST), - finishedAt: Sun Apr 13 2014 17:34:01 GMT-0700 (MST), - name: 'Crunch some important numbers for the marketing department', - startedAt: Sun Apr 13 2014 17:34:00 GMT-0700 (MST), - result: '1982 numbers crunched' -} -``` - -Call `SyncedCron.nextScheduledAtDate(jobName)` to find the date that the job -referenced by `jobName` will run next. - -Call `SyncedCron.remove(jobName)` to remove and stop running the job referenced by jobName. - -Call `SyncedCron.stop()` to remove and stop all jobs. - -Call `SyncedCron.pause()` to stop all jobs without removing them. The existing jobs can be rescheduled (i.e. restarted) with `SyncedCron.start()`. - -### Configuration - -You can configure SyncedCron with the `config` method. Defaults are: - -``` js - SyncedCron.config({ - // Log job run details to console - log: true, - - // Use a custom logger function (defaults to Meteor's logging package) - logger: null - - // Name of collection to use for synchronisation and logging - collectionName: 'cronHistory', - - // Default to using localTime - utc: false, - - /* - TTL in seconds for history records in collection to expire - NOTE: Unset to remove expiry but ensure you remove the index from - mongo by hand - - ALSO: SyncedCron can't use the `_ensureIndex` command to modify - the TTL index. The best way to modify the default value of - `collectionTTL` is to remove the index by hand (in the mongo shell - run `db.cronHistory.dropIndex({startedAt: 1})`) and re-run your - project. SyncedCron will recreate the index with the updated TTL. - */ - collectionTTL: 172800 - }); -``` - -### Logging - -SyncedCron uses Meteor's `logging` package by default. If you want to use your own logger (for sending to other consumers or similar) you can do so by configuring the `logger` option. - -SyncedCron expects a function as `logger`, and will pass arguments to it for you to take action on. - -```js -var MyLogger = function(opts) { - console.log('Level', opts.level); - console.log('Message', opts.message); - console.log('Tag', opts.tag); -} - -SyncedCron.config({ - logger: MyLogger -}); - -SyncedCron.add({ name: 'Test Job', ... }); -SyncedCron.start(); -``` - -The `opts` object passed to `MyLogger` above includes `level`, `message`, and `tag`. - -- `level` will be one of `info`, `warn`, `error`, `debug`. -- `message` is something like `Scheduled "Test Job" next run @Fri Mar 13 2015 10:15:00 GMT+0100 (CET)`. -- `tag` will always be `"SyncedCron"` (handy for filtering). - - -## Caveats - -Beware, SyncedCron probably won't work as expected on certain shared hosting providers that shutdown app instances when they aren't receiving requests (like Heroku's free dyno tier or Meteor free galaxy). - -## Contributing - -Write some code. Write some tests. To run the tests, do: - -``` sh -$ meteor test-packages ./ -``` - -## License - -MIT. (c) Percolate Studio, maintained by Zoltan Olah (@zol). - -Synced Cron was developed as part of the [Verso](http://versoapp.com) project. diff --git a/packages/percolatestudio-synced-cron/example/.meteor/.finished-upgraders b/packages/percolatestudio-synced-cron/example/.meteor/.finished-upgraders deleted file mode 100755 index ee0ed5a316f..00000000000 --- a/packages/percolatestudio-synced-cron/example/.meteor/.finished-upgraders +++ /dev/null @@ -1,6 +0,0 @@ -# This file contains information which helps Meteor properly upgrade your -# app when you run 'meteor update'. You should check it into version control -# with your project. - -notices-for-0.9.0 -notices-for-0.9.1 diff --git a/packages/percolatestudio-synced-cron/example/.meteor/.gitignore b/packages/percolatestudio-synced-cron/example/.meteor/.gitignore deleted file mode 100755 index 40830374235..00000000000 --- a/packages/percolatestudio-synced-cron/example/.meteor/.gitignore +++ /dev/null @@ -1 +0,0 @@ -local diff --git a/packages/percolatestudio-synced-cron/example/.meteor/.id b/packages/percolatestudio-synced-cron/example/.meteor/.id deleted file mode 100755 index 536ae724204..00000000000 --- a/packages/percolatestudio-synced-cron/example/.meteor/.id +++ /dev/null @@ -1,7 +0,0 @@ -# This file contains a token that is unique to your project. -# Check it into your repository along with the rest of this directory. -# It can be used for purposes such as: -# - ensuring you don't accidentally deploy one app on top of another -# - providing package authors with aggregated statistics - -18n88nbddjnt450w00w diff --git a/packages/percolatestudio-synced-cron/example/.meteor/packages b/packages/percolatestudio-synced-cron/example/.meteor/packages deleted file mode 100755 index afc9e5d4e0c..00000000000 --- a/packages/percolatestudio-synced-cron/example/.meteor/packages +++ /dev/null @@ -1,9 +0,0 @@ -# Meteor packages used by this project, one per line. -# -# 'meteor add' and 'meteor remove' will edit this file for you, -# but you can also edit it by hand. - -standard-app-packages -autopublish -insecure -percolate:synced-cron diff --git a/packages/percolatestudio-synced-cron/example/.meteor/release b/packages/percolatestudio-synced-cron/example/.meteor/release deleted file mode 100755 index 8f6f45d1740..00000000000 --- a/packages/percolatestudio-synced-cron/example/.meteor/release +++ /dev/null @@ -1 +0,0 @@ -METEOR@0.9.1.1 diff --git a/packages/percolatestudio-synced-cron/example/.meteor/versions b/packages/percolatestudio-synced-cron/example/.meteor/versions deleted file mode 100755 index 89cf11cf19e..00000000000 --- a/packages/percolatestudio-synced-cron/example/.meteor/versions +++ /dev/null @@ -1,46 +0,0 @@ -application-configuration@1.0.1 -autopublish@1.0.0 -autoupdate@1.0.6 -binary-heap@1.0.0 -blaze-tools@1.0.0 -blaze@2.0.0 -callback-hook@1.0.0 -check@1.0.0 -ctl-helper@1.0.3 -ctl@1.0.1 -ddp@1.0.8 -deps@1.0.3 -ejson@1.0.1 -follower-livedata@1.0.1 -geojson-utils@1.0.0 -html-tools@1.0.0 -htmljs@1.0.0 -id-map@1.0.0 -insecure@1.0.0 -jquery@1.0.0 -json@1.0.0 -livedata@1.0.9 -logging@1.0.2 -meteor-platform@1.0.2 -meteor@1.0.3 -minifiers@1.0.2 -minimongo@1.0.2 -mongo@1.0.4 -observe-sequence@1.0.2 -ordered-dict@1.0.0 -percolate:synced-cron@1.2.0 -random@1.0.0 -reactive-dict@1.0.2 -reactive-var@1.0.1 -reload@1.0.1 -retry@1.0.0 -routepolicy@1.0.0 -session@1.0.1 -spacebars-compiler@1.0.2 -spacebars@1.0.1 -standard-app-packages@1.0.1 -templating@1.0.5 -tracker@1.0.2 -ui@1.0.2 -underscore@1.0.0 -webapp@1.0.3 diff --git a/packages/percolatestudio-synced-cron/example/example.html b/packages/percolatestudio-synced-cron/example/example.html deleted file mode 100755 index 3f05c834477..00000000000 --- a/packages/percolatestudio-synced-cron/example/example.html +++ /dev/null @@ -1,13 +0,0 @@ - - example - - - - {{> hello}} - - - diff --git a/packages/percolatestudio-synced-cron/example/example.js b/packages/percolatestudio-synced-cron/example/example.js deleted file mode 100755 index 9f4a86d1b92..00000000000 --- a/packages/percolatestudio-synced-cron/example/example.js +++ /dev/null @@ -1,41 +0,0 @@ -if (Meteor.isClient) { - Template.hello.greeting = function () { - return "Welcome to example."; - }; - - Template.hello.events({ - 'click input': function () { - // template data, if any, is available in 'this' - if (typeof console !== 'undefined') - console.log("You pressed the button"); - } - }); -} - -if (Meteor.isServer) { - // optionally set the collection's name that synced cron will use - SyncedCron.config({ - collectionName: 'somethingDifferent' - }); - - SyncedCron.add({ - name: 'Crunch some important numbers for the marketing department', - schedule: function(parser) { - // parser is a later.parse object - return parser.text('every 5 seconds'); - }, - job: function(intendedAt) { - console.log('crunching numbers'); - console.log('job should be running at:'); - console.log(intendedAt); - } - }); - - Meteor.startup(function () { - // code to run on server at startup - SyncedCron.start(); - - // Stop jobs after 15 seconds - Meteor.setTimeout(function() { SyncedCron.stop(); }, 15 * 1000); - }); -} diff --git a/packages/percolatestudio-synced-cron/example/packages/synced-cron b/packages/percolatestudio-synced-cron/example/packages/synced-cron deleted file mode 120000 index c25bddb6dd4..00000000000 --- a/packages/percolatestudio-synced-cron/example/packages/synced-cron +++ /dev/null @@ -1 +0,0 @@ -../.. \ No newline at end of file diff --git a/packages/percolatestudio-synced-cron/package.js b/packages/percolatestudio-synced-cron/package.js deleted file mode 100755 index 5b51477129e..00000000000 --- a/packages/percolatestudio-synced-cron/package.js +++ /dev/null @@ -1,20 +0,0 @@ -Package.describe({ - summary: "Allows you to define and run scheduled jobs across multiple servers.", - version: "1.2.1", - name: "percolate:synced-cron", - git: "https://github.com/percolatestudio/meteor-synced-cron.git" -}); - -Npm.depends({later: "1.1.6"}); - -Package.onUse(function (api) { - api.versionsFrom('METEOR@0.9.1.1'); - api.use(['underscore', 'check', 'mongo', 'logging'], 'server'); - api.add_files(['synced-cron-server.js'], "server"); - api.export('SyncedCron', 'server'); -}); - -Package.onTest(function (api) { - api.use(['percolate:synced-cron', 'tinytest']); - api.add_files('synced-cron-tests.js', ['server']); -}); diff --git a/packages/percolatestudio-synced-cron/synced-cron-server.js b/packages/percolatestudio-synced-cron/synced-cron-server.js deleted file mode 100755 index 15c3a8490da..00000000000 --- a/packages/percolatestudio-synced-cron/synced-cron-server.js +++ /dev/null @@ -1,325 +0,0 @@ -// A package for running jobs synchronized across multiple processes -SyncedCron = { - _entries: {}, - running: false, - options: { - //Log job run details to console - log: true, - - logger: null, - - //Name of collection to use for synchronisation and logging - collectionName: 'cronHistory', - - //Default to using localTime - utc: false, - - //TTL in seconds for history records in collection to expire - //NOTE: Unset to remove expiry but ensure you remove the index from - //mongo by hand - collectionTTL: 172800 - }, - config: function(opts) { - this.options = _.extend({}, this.options, opts); - } -} - -Later = Npm.require('later'); - -/* - Logger factory function. Takes a prefix string and options object - and uses an injected `logger` if provided, else falls back to - Meteor's `Log` package. - - Will send a log object to the injected logger, on the following form: - - message: String - level: String (info, warn, error, debug) - tag: 'SyncedCron' -*/ -function createLogger(prefix) { - check(prefix, String); - - // Return noop if logging is disabled. - if(SyncedCron.options.log === false) { - return function() {}; - } - - return function(level, message) { - check(level, Match.OneOf('info', 'error', 'warn', 'debug')); - check(message, String); - - var logger = SyncedCron.options && SyncedCron.options.logger; - - if(logger && _.isFunction(logger)) { - - logger({ - level: level, - message: message, - tag: prefix - }); - - } else { - Log[level]({ message: prefix + ': ' + message }); - } - } -} - -var log; - -Meteor.startup(function() { - var options = SyncedCron.options; - - log = createLogger('SyncedCron'); - - ['info', 'warn', 'error', 'debug'].forEach(function(level) { - log[level] = _.partial(log, level); - }); - - // Don't allow TTL less than 5 minutes so we don't break synchronization - var minTTL = 300; - - // Use UTC or localtime for evaluating schedules - if (options.utc) - Later.date.UTC(); - else - Later.date.localTime(); - - // collection holding the job history records - SyncedCron._collection = new Mongo.Collection(options.collectionName); - SyncedCron._collection._ensureIndex({intendedAt: 1, name: 1}, {unique: true}); - - if (options.collectionTTL) { - if (options.collectionTTL > minTTL) - SyncedCron._collection._ensureIndex({startedAt: 1 }, - { expireAfterSeconds: options.collectionTTL } ); - else - log.warn('Not going to use a TTL that is shorter than:' + minTTL); - } -}); - -var scheduleEntry = function(entry) { - var schedule = entry.schedule(Later.parse); - entry._timer = - SyncedCron._laterSetInterval(SyncedCron._entryWrapper(entry), schedule); - - log.info('Scheduled "' + entry.name + '" next run @' - + Later.schedule(schedule).next(1)); -} - -// add a scheduled job -// SyncedCron.add({ -// name: String, //*required* unique name of the job -// schedule: function(laterParser) {},//*required* when to run the job -// job: function() {}, //*required* the code to run -// }); -SyncedCron.add = function(entry) { - check(entry.name, String); - check(entry.schedule, Function); - check(entry.job, Function); - - // check - if (!this._entries[entry.name]) { - this._entries[entry.name] = entry; - - // If cron is already running, start directly. - if (this.running) { - scheduleEntry(entry); - } - } -} - -// Start processing added jobs -SyncedCron.start = function() { - var self = this; - - Meteor.startup(function() { - // Schedule each job with later.js - _.each(self._entries, function(entry) { - scheduleEntry(entry); - }); - self.running = true; - }); -} - -// Return the next scheduled date of the first matching entry or undefined -SyncedCron.nextScheduledAtDate = function(jobName) { - var entry = this._entries[jobName]; - - if (entry) - return Later.schedule(entry.schedule(Later.parse)).next(1); -} - -// Remove and stop the entry referenced by jobName -SyncedCron.remove = function(jobName) { - var entry = this._entries[jobName]; - - if (entry) { - if (entry._timer) - entry._timer.clear(); - - delete this._entries[jobName]; - log.info('Removed "' + entry.name); - } -} - -// Pause processing, but do not remove jobs so that the start method will -// restart existing jobs -SyncedCron.pause = function() { - if (this.running) { - _.each(this._entries, function(entry) { - entry._timer.clear(); - }); - this.running = false; - } -} - -// Stop processing and remove ALL jobs -SyncedCron.stop = function() { - _.each(this._entries, function(entry, name) { - SyncedCron.remove(name); - }); - this.running = false; -} - -// The meat of our logic. Checks if the specified has already run. If not, -// records that it's running the job, runs it, and records the output -SyncedCron._entryWrapper = function(entry) { - var self = this; - - return function(intendedAt) { - var jobHistory = { - intendedAt: intendedAt, - name: entry.name, - startedAt: new Date() - }; - - // If we have a dup key error, another instance has already tried to run - // this job. - try { - jobHistory._id = self._collection.insert(jobHistory); - } catch(e) { - // http://www.mongodb.org/about/contributors/error-codes/ - // 11000 == duplicate key error - if (e.name === 'MongoError' && e.code === 11000) { - log.info('Not running "' + entry.name + '" again.'); - return; - } - - throw e; - }; - - // run and record the job - try { - log.info('Starting "' + entry.name + '".'); - var output = entry.job(intendedAt); // <- Run the actual job - - log.info('Finished "' + entry.name + '".'); - self._collection.update({_id: jobHistory._id}, { - $set: { - finishedAt: new Date(), - result: output - } - }); - } catch(e) { - log.info('Exception "' + entry.name +'" ' + e.stack); - self._collection.update({_id: jobHistory._id}, { - $set: { - finishedAt: new Date(), - error: e.stack - } - }); - } - }; -} - -// for tests -SyncedCron._reset = function() { - this._entries = {}; - this._collection.remove({}); - this.running = false; -} - -// --------------------------------------------------------------------------- -// The following two functions are lifted from the later.js package, however -// I've made the following changes: -// - Use Meteor.setTimeout and Meteor.clearTimeout -// - Added an 'intendedAt' parameter to the callback fn that specifies the precise -// time the callback function *should* be run (so we can co-ordinate jobs) -// between multiple, potentially laggy and unsynced machines - -// From: https://github.com/bunkat/later/blob/master/src/core/setinterval.js -SyncedCron._laterSetInterval = function(fn, sched) { - - var t = SyncedCron._laterSetTimeout(scheduleTimeout, sched), - done = false; - - /** - * Executes the specified function and then sets the timeout for the next - * interval. - */ - function scheduleTimeout(intendedAt) { - if(!done) { - fn(intendedAt); - t = SyncedCron._laterSetTimeout(scheduleTimeout, sched); - } - } - - return { - - /** - * Clears the timeout. - */ - clear: function() { - done = true; - t.clear(); - } - - }; - -}; - -// From: https://github.com/bunkat/later/blob/master/src/core/settimeout.js -SyncedCron._laterSetTimeout = function(fn, sched) { - - var s = Later.schedule(sched), t; - scheduleTimeout(); - - /** - * Schedules the timeout to occur. If the next occurrence is greater than the - * max supported delay (2147483647 ms) than we delay for that amount before - * attempting to schedule the timeout again. - */ - function scheduleTimeout() { - var now = Date.now(), - next = s.next(2, now), - diff = next[0].getTime() - now, - intendedAt = next[0]; - - // minimum time to fire is one second, use next occurrence instead - if(diff < 1000) { - diff = next[1].getTime() - now; - intendedAt = next[1]; - } - - if(diff < 2147483647) { - t = Meteor.setTimeout(function() { fn(intendedAt); }, diff); - } - else { - t = Meteor.setTimeout(scheduleTimeout, 2147483647); - } - } - - return { - - /** - * Clears the timeout. - */ - clear: function() { - Meteor.clearTimeout(t); - } - - }; - -}; -// --------------------------------------------------------------------------- diff --git a/packages/percolatestudio-synced-cron/synced-cron-tests.js b/packages/percolatestudio-synced-cron/synced-cron-tests.js deleted file mode 100755 index 78101b34d6a..00000000000 --- a/packages/percolatestudio-synced-cron/synced-cron-tests.js +++ /dev/null @@ -1,216 +0,0 @@ -Later = Npm.require('later'); - -Later.date.localTime(); // corresponds to SyncedCron.options.utc: true; - -var TestEntry = { - name: 'Test Job', - schedule: function(parser) { - return parser.cron('15 10 * * ? *'); // not required - }, - job: function() { - return 'ran'; - } -}; - -Tinytest.add('Syncing works', function(test) { - SyncedCron._reset(); - test.equal(SyncedCron._collection.find().count(), 0); - - // added the entry ok - SyncedCron.add(TestEntry); - test.equal(_.keys(SyncedCron._entries).length, 1); - - var entry = SyncedCron._entries[TestEntry.name]; - var intendedAt = new Date(); //whatever - - // first run - SyncedCron._entryWrapper(entry)(intendedAt); - test.equal(SyncedCron._collection.find().count(), 1); - var jobHistory1 = SyncedCron._collection.findOne(); - test.equal(jobHistory1.result, 'ran'); - - // second run - SyncedCron._entryWrapper(entry)(intendedAt); - test.equal(SyncedCron._collection.find().count(), 1); // should still be 1 - var jobHistory2 = SyncedCron._collection.findOne(); - test.equal(jobHistory1._id, jobHistory2._id); -}); - -Tinytest.add('Exceptions work', function(test) { - SyncedCron._reset(); - SyncedCron.add(_.extend({}, TestEntry, { - job: function() { - throw new Meteor.Error('Haha, gotcha!'); - } - }) - ); - - var entry = SyncedCron._entries[TestEntry.name]; - var intendedAt = new Date(); //whatever - - // error without result - SyncedCron._entryWrapper(entry)(intendedAt); - test.equal(SyncedCron._collection.find().count(), 1); - var jobHistory1 = SyncedCron._collection.findOne(); - test.equal(jobHistory1.result, undefined); - test.matches(jobHistory1.error, /Haha, gotcha/); -}); - -Tinytest.add('SyncedCron.nextScheduledAtDate works', function(test) { - SyncedCron._reset(); - test.equal(SyncedCron._collection.find().count(), 0); - - // addd 2 entries - SyncedCron.add(TestEntry); - - var entry2 = _.extend({}, TestEntry, { - name: 'Test Job2', - schedule: function(parser) { - return parser.cron('30 11 * * ? *'); - } - }); - SyncedCron.add(entry2); - - test.equal(_.keys(SyncedCron._entries).length, 2); - - SyncedCron.start(); - - var date = SyncedCron.nextScheduledAtDate(entry2.name); - var correctDate = Later.schedule(entry2.schedule(Later.parse)).next(1); - - test.equal(date, correctDate); -}); - -// Tests SyncedCron.remove in the process -Tinytest.add('SyncedCron.stop works', function(test) { - SyncedCron._reset(); - test.equal(SyncedCron._collection.find().count(), 0); - - // addd 2 entries - SyncedCron.add(TestEntry); - - var entry2 = _.extend({}, TestEntry, { - name: 'Test Job2', - schedule: function(parser) { - return parser.cron('30 11 * * ? *'); - } - }); - SyncedCron.add(entry2); - - SyncedCron.start(); - - test.equal(_.keys(SyncedCron._entries).length, 2); - - SyncedCron.stop(); - - test.equal(_.keys(SyncedCron._entries).length, 0); -}); - -Tinytest.add('SyncedCron.pause works', function(test) { - SyncedCron._reset(); - test.equal(SyncedCron._collection.find().count(), 0); - - // addd 2 entries - SyncedCron.add(TestEntry); - - var entry2 = _.extend({}, TestEntry, { - name: 'Test Job2', - schedule: function(parser) { - return parser.cron('30 11 * * ? *'); - } - }); - SyncedCron.add(entry2); - - SyncedCron.start(); - - test.equal(_.keys(SyncedCron._entries).length, 2); - - SyncedCron.pause(); - - test.equal(_.keys(SyncedCron._entries).length, 2); - test.isFalse(SyncedCron.running); - - SyncedCron.start(); - - test.equal(_.keys(SyncedCron._entries).length, 2); - test.isTrue(SyncedCron.running); - -}); - -// Tests SyncedCron.remove in the process -Tinytest.add('SyncedCron.add starts by it self when running', function(test) { - SyncedCron._reset(); - - test.equal(SyncedCron._collection.find().count(), 0); - test.equal(SyncedCron.running, false); - Log._intercept(2); - - SyncedCron.start(); - - test.equal(SyncedCron.running, true); - - // addd 1 entries - SyncedCron.add(TestEntry); - - test.equal(_.keys(SyncedCron._entries).length, 1); - - SyncedCron.stop(); - - var intercepted = Log._intercepted(); - test.equal(intercepted.length, 2); - - test.equal(SyncedCron.running, false); - test.equal(_.keys(SyncedCron._entries).length, 0); -}); - -Tinytest.add('SyncedCron.config can customize the options object', function(test) { - SyncedCron._reset(); - - SyncedCron.config({ - log: false, - collectionName: 'foo', - utc: true, - collectionTTL: 0 - }); - - test.equal(SyncedCron.options.log, false); - test.equal(SyncedCron.options.collectionName, 'foo'); - test.equal(SyncedCron.options.utc, true); - test.equal(SyncedCron.options.collectionTTL, 0); -}); - -Tinytest.addAsync('SyncedCron can log to injected logger', function(test, done) { - SyncedCron._reset(); - - var logger = function() { - test.isTrue(true); - done(); - }; - - SyncedCron.options.logger = logger; - - SyncedCron.add(TestEntry); - SyncedCron.start(); - - SyncedCron.options.logger = null; -}); - -Tinytest.addAsync('SyncedCron should pass correct arguments to logger', function(test, done) { - SyncedCron._reset(); - - var logger = function(opts) { - test.include(opts, 'level'); - test.include(opts, 'message'); - test.include(opts, 'tag'); - test.equal(opts.tag, 'SyncedCron'); - - done(); - }; - - SyncedCron.options.logger = logger; - - SyncedCron.add(TestEntry); - SyncedCron.start(); - - SyncedCron.options.logger = null; -}); diff --git a/packages/percolatestudio-synced-cron/versions.json b/packages/percolatestudio-synced-cron/versions.json deleted file mode 100755 index 5711dabf625..00000000000 --- a/packages/percolatestudio-synced-cron/versions.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "dependencies": [ - [ - "application-configuration", - "1.0.1" - ], - [ - "binary-heap", - "1.0.0" - ], - [ - "callback-hook", - "1.0.0" - ], - [ - "check", - "1.0.0" - ], - [ - "ddp", - "1.0.8" - ], - [ - "ejson", - "1.0.1" - ], - [ - "follower-livedata", - "1.0.1" - ], - [ - "geojson-utils", - "1.0.0" - ], - [ - "id-map", - "1.0.0" - ], - [ - "json", - "1.0.0" - ], - [ - "logging", - "1.0.2" - ], - [ - "meteor", - "1.0.3" - ], - [ - "minimongo", - "1.0.2" - ], - [ - "mongo", - "1.0.4" - ], - [ - "ordered-dict", - "1.0.0" - ], - [ - "random", - "1.0.0" - ], - [ - "retry", - "1.0.0" - ], - [ - "tracker", - "1.0.2" - ], - [ - "underscore", - "1.0.0" - ] - ], - "pluginDependencies": [], - "toolVersion": "meteor-tool@1.0.28", - "format": "1.0" -} \ No newline at end of file