diff --git a/README.md b/README.md
index e6c11bbc..6afe63a8 100644
--- a/README.md
+++ b/README.md
@@ -1547,6 +1547,7 @@ Here you will need to provide:
- A **Group Title** which is used for display purposes.
- A **Hostname Match** which is a regular expression applied to every server hostname (used to automatically add servers to the group).
- A **Server Class** which sets the servers in your group as "Master Eligible" or "Slave Only".
+- A **Maximum Jobs** which is used to determine which servers can run an event that targets this group, only the servers in the group that have less active jobs than the maximum are considered as potential candidates.
Note that "Master Eligible" servers all need to be properly configured and have access to your storage back-end. Meaning, if you opted to use the filesystem, you'll need to make sure it is mounted (via NFS or similar mechanism) on all the servers who could become master. Or, if you opted to use a NoSQL DB such as Couchbase or S3, they need all the proper settings and/or credentials to connect. For more details, see the [Multi-Server Cluster](#multi-server-cluster) section.
diff --git a/bin/install.js b/bin/install.js
index 7446f65a..34ab10a6 100755
--- a/bin/install.js
+++ b/bin/install.js
@@ -15,9 +15,9 @@ var installer_version = '1.1';
var base_dir = '/opt/cronicle';
var log_dir = base_dir + '/logs';
var log_file = '';
-var gh_repo_url = 'http://github.com/jhuckaby/Cronicle';
-var gh_releases_url = 'https://api.github.com/repos/jhuckaby/Cronicle/releases';
-var gh_head_tarball_url = 'https://github.com/jhuckaby/Cronicle/archive/master.tar.gz';
+var gh_repo_url = 'http://github.com/raymatos/Cronicle';
+var gh_releases_url = 'https://api.github.com/repos/raymatos/Cronicle/releases';
+var gh_head_tarball_url = 'https://github.com/raymatos/Cronicle/archive/master.tar.gz';
var print = function(msg) {
process.stdout.write(msg);
diff --git a/htdocs/js/pages/admin/Servers.js b/htdocs/js/pages/admin/Servers.js
index ebd41f1f..6d6cf0d0 100644
--- a/htdocs/js/pages/admin/Servers.js
+++ b/htdocs/js/pages/admin/Servers.js
@@ -281,6 +281,9 @@ Class.add( Page.Admin, {
get_form_table_spacer() +
get_form_table_row('Server Class:', '') +
get_form_table_caption("Select whether servers in the group are eligible to become the master server, or run as slaves only.") +
+ get_form_table_spacer() +
+ get_form_table_row('Maximum Jobs:', '') +
+ get_form_table_caption("Enter a maximum number of jobs per server for this group, 0 for unlimited.") +
'';
app.confirm( '' + (edit ? "Edit Server Group" : "Add Server Group"), html, edit ? "Save Changes" : "Add Group", function(result) {
@@ -297,6 +300,11 @@ Class.add( Page.Admin, {
return app.badField('fe_eg_regexp', "Invalid regular expression: " + err);
}
+ group.max_jobs = parseInt( $('#fe_eg_max_jobs').val() );
+ if (isNaN(group.max_jobs) || group.max_jobs < 0) {
+ return app.badField('fe_eg_max_jobs', "Please enter a non-negative integer for the maximum jobs.");
+ }
+
group.master = parseInt( $('#fe_eg_master').val() );
Dialog.hide();
diff --git a/lib/job.js b/lib/job.js
index 2a68fd16..a832ff6a 100644
--- a/lib/job.js
+++ b/lib/job.js
@@ -148,6 +148,15 @@ module.exports = Class.create({
}
}
+ // filter out the candidates that don't have the capacity
+ var max_jobs = server_group.max_jobs || 0;
+ if (max_jobs > 0) {
+ candidates = candidates.filter( function(server) {
+ var jobs = Tools.findObjectsIdx( job_list, { 'hostname': server.hostname } );
+ return jobs.length < max_jobs;
+ } );
+ }
+
if (!candidates.length) {
return callback( new Error("Could not find any servers for group: " + server_group.title) );
}
diff --git a/lib/test.js b/lib/test.js
index 7e9ede29..1dce76cb 100644
--- a/lib/test.js
+++ b/lib/test.js
@@ -453,7 +453,7 @@ module.exports = {
function testAPICreateServerGroup(test) {
// test app/create_server_group api
var self = this;
- var params = {"title":"del gap","regexp":"dasds","master":0,"session_id":session_id};
+ var params = {"title":"del gap","regexp":"dasds","master":0,"max_jobs":42,"session_id":session_id};
request.json( api_url + '/app/create_server_group', params, function(err, resp, data) {
@@ -472,6 +472,7 @@ module.exports = {
test.ok( !!group, "Data record record is non-null" );
test.ok( group.title == "del gap", "Title is correct" );
test.ok( group.regexp == "dasds", "Regexp is correct" );
+ test.ok( group.max_jobs === 42, "Max jobs is correct" );
test.done();
} );
@@ -1443,6 +1444,77 @@ module.exports = {
}, 500 );
},
+ function testMaxJobsPerServer(test) {
+ // limit the main server group to a certain number of jobs per server
+ // set number of concurrent jobs to unlimited for the event and also allow it to be queued
+ // launch the event multiple times and check that the number of active jobs
+ // is equal to the maximum per server and that the rest are being queued
+ var event_id = this.event_id;
+ var eventQueue = cronicle.eventQueue;
+ var n_queued = 3;
+ var max_jobs = 2;
+
+ async.auto({
+ update_group: function (callback) {
+ var params = {
+ "id": "maingrp",
+ "max_jobs": max_jobs,
+ "session_id": session_id
+ };
+ request.json( api_url + '/app/update_server_group', params, callback );
+ },
+ update_event: function (callback) {
+ var params = {
+ "id": event_id,
+ "session_id": session_id,
+ "queue": 1,
+ "max_children": 0
+ };
+ request.json( api_url + '/app/update_event', params, callback );
+ },
+ event: [ 'update_event', function (results, callback) {
+ storage.listFind( 'global/schedule', { id: event_id }, callback );
+ } ],
+ jobs: [ 'event', function (results, callback) {
+ async.times( max_jobs + n_queued, function(_, next) {
+ var job = Tools.copyHash( results.event[0], true );
+ job.params.script = "#!/bin/sh\nsleep 2s";
+ cronicle.launchOrQueueJob( job, next );
+ }, callback );
+ } ]
+ },
+ function(err, results) {
+ test.ok( !err, "No errors" );
+ test.ok( active_job_count() == max_jobs, "Number of active jobs is correct" );
+ test.ok( eventQueue[event_id] == n_queued, "Number of queued jobs is correct" );
+
+ // kill the processes and wait for the jobs to end
+ delete eventQueue[event_id];
+
+ results.jobs.forEach( function(jobs) {
+ if (jobs.length) {
+ try { process.kill( jobs[0].pid, 9 ); }
+ catch (e) {;}
+ }
+ } );
+
+ async.doWhilst(
+ function (callback) {
+ setTimeout( callback, 100 );
+ },
+ active_job_count,
+ function () {
+ test.done();
+ }
+ );
+
+ function active_job_count() {
+ var active_jobs = cronicle.getAllActiveJobs();
+ return Object.keys(active_jobs).length;
+ }
+ } ); // async.auto
+ },
+
// app/delete_event
function testAPIDeleteEvent(test) {
diff --git a/sample_conf/setup.json b/sample_conf/setup.json
index 3363109e..6e2fe95a 100644
--- a/sample_conf/setup.json
+++ b/sample_conf/setup.json
@@ -82,13 +82,15 @@
"id": "maingrp",
"title": "Master Group",
"regexp": "_HOSTNAME_",
- "master": 1
+ "master": 1,
+ "max_jobs": 0
} ],
[ "listPush", "global/server_groups", {
"id": "allgrp",
"title": "All Servers",
"regexp": ".+",
- "master": 0
+ "master": 0,
+ "max_jobs": 0
} ],
[ "listCreate", "global/servers", {} ],
[ "listPush", "global/servers", {