taskcluster-client
Advanced tools
Comparing version 0.5.1 to 0.6.0
247
apis.json
@@ -186,3 +186,250 @@ { | ||
} | ||
}, | ||
"QueueEvents": { | ||
"referenceUrl": "http://references.taskcluster.net/queue/v1/exchanges.json", | ||
"reference": { | ||
"version": "0.2.0", | ||
"title": "Queue AMQP Exchanges", | ||
"description": "The queue, typically available at `queue.taskcluster.net`, is responsible\nfor accepting tasks and track their state as they are executed by\nworkers. In order ensure they are eventually resolved.\n\nThis document describes AMQP exchanges offered by the queue, which allows\nthird-party listeners to monitor tasks as they progress to resolution.\nThese exchanges targets the following audience:\n * Schedulers, who takes action after tasks are completed,\n * Workers, who wants to listen for new or canceled tasks (optional),\n * Tools, that wants to update their view as task progress.\n\nYou'll notice that all the exchanges in the document shares the same\nrouting key pattern. This makes it very easy to bind to all message about\na certain kind tasks. You should also note that the `routing` property\nof a task will be used in the routing key. This property is user-defined\nand may contain dots (ie. multiple routing words).\n\n**Remark**, if the task-graph scheduler, documented elsewhere, is used to\nscheduler a task-graph, then task submitted will have their `routing` key\nprefixed by `task-graph-scheduler.<taskGraphId>.` this means that the\nfirst two words of the task `routing` key will be\n`'task-graph-scheduler'` and `taskGraphId`. This is useful if you're\ninterested in updates about a specific task-graph, and it is necessary\nto know if you're binding to the `task.routing` key and submitting tasks\nthrough the task-graph scheduler. See documentation for task-graph\nscheduler for more details.", | ||
"exchangePrefix": "queue/v1/", | ||
"entries": [ | ||
{ | ||
"type": "topic-exchange", | ||
"exchange": "task-pending", | ||
"name": "taskPending", | ||
"title": "Task Pending Messages", | ||
"description": "When a task becomes `pending` a message is posted to this exchange.\n\nThis is useful for workers who doesn't want to constantly poll the queue\nfor new tasks. The queue will also be authority for task states and\nclaims. But using this exchange workers should be able to distribute work\nefficiently and they would be able to reduce their polling interval\nsignificantly without affecting general responsiveness.", | ||
"routingKey": [ | ||
{ | ||
"name": "taskId", | ||
"summary": "`taskId` for the task this message concerns", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "runId", | ||
"summary": "`runId` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 3 | ||
}, | ||
{ | ||
"name": "workerGroup", | ||
"summary": "`workerGroup` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "workerId", | ||
"summary": "`workerId` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "provisionerId", | ||
"summary": "`provisionerId` this task is targeted at.", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "workerType", | ||
"summary": "`workerType` this task must run on.", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "routing", | ||
"summary": "task-specific routing key (`task.routing`).", | ||
"multipleWords": true, | ||
"required": true, | ||
"maxSize": 128 | ||
} | ||
], | ||
"schema": "http://schemas.taskcluster.net/queue/v1/task-pending-message.json#" | ||
}, | ||
{ | ||
"type": "topic-exchange", | ||
"exchange": "task-running", | ||
"name": "taskRunning", | ||
"title": "Task Running Messages", | ||
"description": "Whenever a task is claimed by a worker, a run is started on the worker,\nand a message is posted on this exchange.\n\n**Notice**, that the `logsUrl` may return `404` during the run, but by\nthe end of the run the `logsUrl` will be valid. But this may not have\nhappened when this message is posted.\n\nThe idea is that workers can choose to upload the `logs.json` file as the\nfirst thing they do, in which case it'll often be available after a few\nminutes. This is useful if the worker supports live logging.", | ||
"routingKey": [ | ||
{ | ||
"name": "taskId", | ||
"summary": "`taskId` for the task this message concerns", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "runId", | ||
"summary": "`runId` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 3 | ||
}, | ||
{ | ||
"name": "workerGroup", | ||
"summary": "`workerGroup` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "workerId", | ||
"summary": "`workerId` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "provisionerId", | ||
"summary": "`provisionerId` this task is targeted at.", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "workerType", | ||
"summary": "`workerType` this task must run on.", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "routing", | ||
"summary": "task-specific routing key (`task.routing`).", | ||
"multipleWords": true, | ||
"required": true, | ||
"maxSize": 128 | ||
} | ||
], | ||
"schema": "http://schemas.taskcluster.net/queue/v1/task-running-message.json#" | ||
}, | ||
{ | ||
"type": "topic-exchange", | ||
"exchange": "task-completed", | ||
"name": "taskCompleted", | ||
"title": "Task Completed Messages", | ||
"description": "When a task is completed by a worker a message is posted this exchange.\nThis message is routed using the `run-id`, `worker-group` and `worker-id`\nthat completed the task. But information about additional runs is also\navailable from the task status structure.\n\nUpon task completion a result structure is made available, you'll find\nthe url in the `resultURL` property. See _task storage_ documentation for\ndetails on the format of the file available through `resultUrl`.", | ||
"routingKey": [ | ||
{ | ||
"name": "taskId", | ||
"summary": "`taskId` for the task this message concerns", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "runId", | ||
"summary": "`runId` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 3 | ||
}, | ||
{ | ||
"name": "workerGroup", | ||
"summary": "`workerGroup` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "workerId", | ||
"summary": "`workerId` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "provisionerId", | ||
"summary": "`provisionerId` this task is targeted at.", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "workerType", | ||
"summary": "`workerType` this task must run on.", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "routing", | ||
"summary": "task-specific routing key (`task.routing`).", | ||
"multipleWords": true, | ||
"required": true, | ||
"maxSize": 128 | ||
} | ||
], | ||
"schema": "http://schemas.taskcluster.net/queue/v1/task-completed-message.json#" | ||
}, | ||
{ | ||
"type": "topic-exchange", | ||
"exchange": "task-failed", | ||
"name": "taskFailed", | ||
"title": "Task Failed Messages", | ||
"description": "Whenever a task is concluded to be failed a message is posted to this\nexchange. This happens if the task isn't completed before its `deadlìne`,\nall retries failed (i.e. workers stopped responding) or the task was\ncanceled by another entity.\n\nThe specific _reason_ is evident from that task status structure, refer\nto the `reason` property.", | ||
"routingKey": [ | ||
{ | ||
"name": "taskId", | ||
"summary": "`taskId` for the task this message concerns", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "runId", | ||
"summary": "`runId` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 3 | ||
}, | ||
{ | ||
"name": "workerGroup", | ||
"summary": "`workerGroup` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "workerId", | ||
"summary": "`workerId` of latest run for the task, `_` if no run is exists for the task.", | ||
"multipleWords": false, | ||
"required": false, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "provisionerId", | ||
"summary": "`provisionerId` this task is targeted at.", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "workerType", | ||
"summary": "`workerType` this task must run on.", | ||
"multipleWords": false, | ||
"required": true, | ||
"maxSize": 22 | ||
}, | ||
{ | ||
"name": "routing", | ||
"summary": "task-specific routing key (`task.routing`).", | ||
"multipleWords": true, | ||
"required": true, | ||
"maxSize": 128 | ||
} | ||
], | ||
"schema": "http://schemas.taskcluster.net/queue/v1/task-failed-message.json#" | ||
} | ||
] | ||
} | ||
} | ||
} |
@@ -28,4 +28,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public | ||
* } | ||
* baseUrl: 'http://.../v1' // API baseUrl, default is taken from reference | ||
* baseUrl: 'http://.../v1' // baseUrl for API requests | ||
* exchangePrefix: 'queue/v1/' // exchangePrefix prefix | ||
* } | ||
* | ||
* `baseUrl` and `exchangePrefix` defaults to values from reference. | ||
*/ | ||
@@ -36,3 +39,4 @@ exports.createClient = function(reference) { | ||
this._options = _.defaults(options || {}, { | ||
baseUrl: reference.baseUrl | ||
baseUrl: reference.baseUrl || '', | ||
exchangePrefix: reference.exchangePrefix || '' | ||
}, _defaultOptions); | ||
@@ -111,2 +115,44 @@ }; | ||
// For each topic-exchange entry | ||
reference.entries.filter(function(entry) { | ||
return entry.type === 'topic-exchange'; | ||
}).forEach(function(entry) { | ||
// Create function for routing-key pattern construction | ||
Client.prototype[entry.name] = function(routingKeyPattern) { | ||
if (typeof(routingKeyPattern) !== 'string') { | ||
// Allow for empty routing key patterns | ||
if (routingKeyPattern === undefined || | ||
routingKeyPattern === null) { | ||
routingKeyPattern = {}; | ||
} | ||
// Check that the routing key pattern is an object | ||
assert(routingKeyPattern instanceof Object, | ||
"routingKeyPattern must be an object"); | ||
// Construct routingkey pattern as string from reference | ||
routingKeyPattern = entry.routingKey.map(function(key) { | ||
var value = routingKeyPattern[key.name]; | ||
if (typeof(value) === 'string') { | ||
assert(key.multipleWords || value.indexOf('.') === -1, | ||
"routingKey pattern '" + value + "' for " + key.name + | ||
" cannot contain dots as it does not hold multiple words"); | ||
return value; | ||
} else { | ||
assert(value === null || value === undefined, | ||
"Value: '" + value + "' is not supported as routingKey "+ | ||
"pattern for " + key.name); | ||
return key.multipleWords ? '#' : '*'; | ||
} | ||
}).join('.'); | ||
} | ||
// Return values necessary to bind with EventHandler | ||
return { | ||
exchange: this._options.exchangePrefix + entry.exchange, | ||
routingKeyPattern: routingKeyPattern, | ||
routingKeyReference: _.cloneDeep(entry.routingKey) | ||
}; | ||
}; | ||
}); | ||
// Return client class | ||
@@ -141,1 +187,4 @@ return Client; | ||
}; | ||
// Export listener | ||
exports.Listener = require('./listener'); |
{ | ||
"name": "taskcluster-client", | ||
"version": "0.5.1", | ||
"version": "0.6.0", | ||
"author": "Jonas Finnemann Jensen <jopsen@gmail.com>", | ||
@@ -8,3 +8,3 @@ "description": "Client for interfacing taskcluster components", | ||
"scripts": { | ||
"test": "mocha test/client_test.js" | ||
"test": "./test/runtests.sh" | ||
}, | ||
@@ -20,3 +20,5 @@ "repository": { | ||
"superagent-promise": "0.1.0", | ||
"superagent-hawk": "0.0.3" | ||
"superagent-hawk": "0.0.3", | ||
"amqplib": "0.2.0", | ||
"slugid": "1.0.1" | ||
}, | ||
@@ -23,0 +25,0 @@ "devDependencies": { |
124
README.md
# TaskCluster Client [![Build Status](https://travis-ci.org/taskcluster/taskcluster-client.svg?branch=master)](https://travis-ci.org/taskcluster/taskcluster-client) | ||
_A taskcluster client library for node.js._ | ||
## Usage | ||
This client library is generated from the auto-generated API reference. | ||
@@ -10,2 +9,8 @@ You can create a Client class from a JSON reference object at runtime using | ||
## Calling API End-Points | ||
To invoke an API end-point instantiate a taskcluster Client class, these are | ||
classes can be created from a JSON reference object, but a number of them are | ||
also built-in to this library. In the following example we instantiate an | ||
instance of the `Queue` Client class and use to to create a task. | ||
```js | ||
@@ -33,2 +38,56 @@ var taskcluster = require('taskcluster-client'); | ||
## Listening for Events | ||
Many TaskCluster components publishes messages about current events over AMQP. | ||
The JSON reference object also contains meta-data about declared AMQP topic | ||
exchanges and their routing key construction. This is designed to make it easy | ||
to construct routing key patterns and parse routing keys from incoming messages. | ||
The following example create a `listener` and instantiate an instance of | ||
the Client class `QueueEvents` which we use to find the exchange and create | ||
a routing pattern to listen for completion of a specific task. The | ||
`taskCompleted` method will construct a routing key pattern by using `*` or `#` | ||
for missing entries, pending on whether or not they are single word or | ||
multi-key entries. | ||
```js | ||
var taskcluster = require('taskcluster-client'); | ||
// Create a listener (this creates a queue on AMQP) | ||
var listener = new taskcluster.Listener({ | ||
connectionString: 'amqp://...' | ||
}); | ||
// Instantiate the QueueEvents Client class | ||
var queueEvents = new taskcluster.QueueEvents(); | ||
// Bind to task-completed events from queue that matches routing key pattern: | ||
// '<myTaskId>.*.*.*.*.*.#' | ||
listener.bind(queueEvents.taskCompleted({taskId: '<myTaskId>'})); | ||
// Listen for messages | ||
listener.on('message', function(message) { | ||
message.exchange // Exchange from which message came | ||
message.payload // Documented on docs.taskcluster.net | ||
message.routingKey // Message routing key in string format | ||
message.routing.taskId // Element from parsed routing key | ||
message.routing.runId // ... | ||
message.redelivered // True, if message has been nack'ed and requeued | ||
return new Promise(...); | ||
}); | ||
// Start listening for events | ||
listener.connect().then(function() { | ||
// Now listening | ||
}); | ||
``` | ||
The listener creates a AMQP queue, on the server side and subscribes to messages | ||
on the queue. It's possible to use named queues, see details below. For details | ||
on routing key entries refer to documentation on | ||
[docs.taskcluster.net](docs.taskcluster.net). | ||
**Remark,** API end-points and AMQP exchanges are typically documented in | ||
separate reference files. For this reason they also have separate Client | ||
classes, even if they are from the same component. | ||
## Documentation | ||
@@ -73,2 +132,13 @@ The set of API entries listed below is generated from the builtin references. | ||
### Exchanges in `taskcluster.QueueEvents` | ||
```js | ||
// Create QueueEvents client instance with default exchangePrefix: | ||
// - queue/v1/ | ||
var queueEvents = new taskcluster.QueueEvents(options); | ||
``` | ||
* `queueEvents.taskPending(routingKeyPattern) : binding-info` | ||
* `queueEvents.taskRunning(routingKeyPattern) : binding-info` | ||
* `queueEvents.taskCompleted(routingKeyPattern) : binding-info` | ||
* `queueEvents.taskFailed(routingKeyPattern) : binding-info` | ||
<!-- END OF GENERATED DOCS --> | ||
@@ -87,5 +157,3 @@ | ||
// Instantiate an instance of MyClient | ||
var myClient = new MyClient({ | ||
credentials: {...} | ||
}); | ||
var myClient = new MyClient(options); | ||
@@ -98,3 +166,8 @@ // Make a request with a method on myClient | ||
## Configuring API BaseUrls | ||
## Configuration of API Invocations | ||
There is a number of configuration options for Client which affects invocation | ||
of API end-points. These are useful if using a non-default server, for example | ||
when setting up a staging area or testing locally. | ||
### Configuring API BaseUrls | ||
If you use the builtin API Client classes documented above you can configure | ||
@@ -110,3 +183,3 @@ the `baseUrl` when creating an instance of the client. As illustrated below: | ||
## Configuring Credentials | ||
### Configuring Credentials | ||
When creating an instance of a Client class the credentials can be provided | ||
@@ -139,3 +212,3 @@ in options. For example: | ||
## Delegated Authorization | ||
### Delegated Authorization | ||
If your client has the scope `auth:can-delegate` you can send requests with | ||
@@ -168,2 +241,39 @@ a scope set different from the one you have. This is useful when the | ||
## Configuration of Exchange Bindings | ||
When a taskcluster Client class is instantiated the option `exchangePrefix` may | ||
be given. This will replace the default `exchangePrefix`. This can be useful if | ||
deploying a staging area or similar. See example below: | ||
```js | ||
// Instantiate the QueueEvents Client class | ||
var queueEvents = new taskcluster.QueueEvents({ | ||
exchangePrefix: 'staging-queue/v1/' | ||
}); | ||
// This listener will now bind to: staging-queue/v1/task-completed | ||
listener.bind(queueEvents.taskCompleted({taskId: '<myTaskId>'})); | ||
``` | ||
## Using the Listener | ||
TODO: | ||
``` | ||
var listener = new taskcluster.Listener({ | ||
prefetch: 5, // Number of tasks to process in parallel | ||
connectionString: 'amqp://...', // AMQP connection string | ||
// If no queue name is given, the queue is: | ||
// exclusive, autodeleted and non-durable | ||
// If a queue name is given, the queue is: | ||
// durable, not auto-deleted and non-exclusive | ||
queueName: 'my-queue', // Queue name, undefined if none | ||
maxLength: 0, // Max allowed queue size | ||
}); | ||
listener.connect().then(...); // Setup listener and start | ||
listener.pause().then(...); // Pause retrieval of new messages | ||
listener.resume().then(...); // Start getting new messages | ||
listener.close(); // Disconnect from AMQP | ||
``` | ||
## Updating Builtin APIs | ||
@@ -170,0 +280,0 @@ When releasing a new version of the `taskcluster-client` library, we should |
@@ -22,2 +22,7 @@ #!/usr/bin/env node | ||
/** Find instance name by making first character lower-case */ | ||
var instanceName = function(name) { | ||
return name[0].toLowerCase() + name.substr(1); | ||
}; | ||
var apis = loadApis(); | ||
@@ -27,3 +32,11 @@ | ||
DOCS_START_MARKER | ||
].concat(_.keys(apis).map(function(name) { | ||
]; | ||
// Generate documentation for methods | ||
docs = docs.concat(_.keys(apis).filter(function(name) { | ||
// Find component that hold functions | ||
return apis[name].reference.entries.some(function(entry) { | ||
return entry.type === 'function'; | ||
}); | ||
}).map(function(name) { | ||
var api = apis[name]; | ||
@@ -36,3 +49,3 @@ return [ | ||
"// - " + api.reference.baseUrl, | ||
"var " + name.toLowerCase() + " = new taskcluster." + name + "(options);", | ||
"var " + instanceName(name) + " = new taskcluster." + name + "(options);", | ||
"```" | ||
@@ -50,9 +63,37 @@ ].concat(api.reference.entries.filter(function(entry) { | ||
} | ||
return " * `" + name.toLowerCase() + "." + entry.name + | ||
return " * `" + instanceName(name) + "." + entry.name + | ||
"(" + args.join(', ') + ") : " + retval + "`"; | ||
})).join('\n'); | ||
}).concat([ | ||
})); | ||
// Generate documentation for exchanges | ||
docs = docs.concat(_.keys(apis).filter(function(name) { | ||
// Find component that hold functions | ||
return apis[name].reference.entries.some(function(entry) { | ||
return entry.type === 'topic-exchange'; | ||
}); | ||
}).map(function(name) { | ||
var api = apis[name]; | ||
return [ | ||
"", | ||
"### Exchanges in `taskcluster." + name + "`", | ||
"```js", | ||
"// Create " + name + " client instance with default exchangePrefix:", | ||
"// - " + api.reference.exchangePrefix, | ||
"var " + instanceName(name) + " = new taskcluster." + name + "(options);", | ||
"```" | ||
].concat(api.reference.entries.filter(function(entry) { | ||
return entry.type === 'topic-exchange'; | ||
}).map(function(entry) { | ||
return " * `" + instanceName(name) + "." + entry.name + | ||
"(routingKeyPattern) : binding-info`"; | ||
})).join('\n'); | ||
})); | ||
docs = docs.concat([ | ||
"", | ||
DOCS_END_MARKER | ||
])).join('\n') | ||
]).join('\n'); | ||
@@ -59,0 +100,0 @@ // Load readme |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
88744
19
1560
282
7
+ Addedamqplib@0.2.0
+ Addedslugid@1.0.1
+ Addedamqplib@0.2.0(transitive)
+ Addedbitsyntax@0.0.4(transitive)
+ Addedbuffer-more-ints@0.0.2(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@0.0.1(transitive)
+ Addedreadable-stream@1.1.14(transitive)
+ Addedslugid@1.0.1(transitive)
+ Addedstring_decoder@0.10.31(transitive)
+ Addeduuid@1.4.1(transitive)
+ Addedwhen@2.1.1(transitive)