feat: initial commit - Phase 1 & 2 core features

This commit is contained in:
hiderfong
2026-04-22 17:07:33 +08:00
commit 1773bda06b
25005 changed files with 6252106 additions and 0 deletions
@@ -0,0 +1,23 @@
import os
import tornado.web
from ..utils import strtobool
from ..views import BaseHandler
class BaseApiHandler(BaseHandler):
def prepare(self):
enable_api = strtobool(os.environ.get(
'FLOWER_UNAUTHENTICATED_API') or "false")
if not (self.application.options.basic_auth or self.application.options.auth) and not enable_api:
raise tornado.web.HTTPError(
401, "FLOWER_UNAUTHENTICATED_API environment variable is required to enable API without authentication")
def write_error(self, status_code, **kwargs):
exc_info = kwargs.get('exc_info')
log_message = exc_info[1].log_message
if log_message:
self.write(log_message)
self.set_status(status_code)
self.finish()
@@ -0,0 +1,531 @@
import logging
from tornado import web
from . import BaseApiHandler
logger = logging.getLogger(__name__)
class ControlHandler(BaseApiHandler):
def is_worker(self, workername):
return workername and workername in self.application.workers
def error_reason(self, workername, response):
"extracts error message from response"
for res in response:
try:
return res[workername].get('error', 'Unknown reason')
except KeyError:
pass
logger.error("Failed to extract error reason from '%s'", response)
return 'Unknown reason'
class WorkerShutDown(ControlHandler):
@web.authenticated
def post(self, workername):
"""
Shut down a worker
**Example request**:
.. sourcecode:: http
POST /api/worker/shutdown/celery@worker2 HTTP/1.1
Content-Length: 0
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 29
Content-Type: application/json; charset=UTF-8
{
"message": "Shutting down!"
}
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 404: unknown worker
"""
if not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
logger.info("Shutting down '%s' worker", workername)
self.capp.control.broadcast('shutdown', destination=[workername])
self.write(dict(message="Shutting down!"))
class WorkerPoolRestart(ControlHandler):
@web.authenticated
def post(self, workername):
"""
Restart worker's pool
**Example request**:
.. sourcecode:: http
POST /api/worker/pool/restart/celery@worker2 HTTP/1.1
Content-Length: 0
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 56
Content-Type: application/json; charset=UTF-8
{
"message": "Restarting 'celery@worker2' worker's pool"
}
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 403: pool restart is not enabled (see CELERYD_POOL_RESTARTS)
:statuscode 404: unknown worker
"""
if not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
logger.info("Restarting '%s' worker's pool", workername)
response = self.capp.control.broadcast(
'pool_restart', arguments={'reload': False},
destination=[workername], reply=True)
if response and 'ok' in response[0][workername]:
self.write(dict(message=f"Restarting '{workername}' worker's pool"))
else:
logger.error(response)
self.set_status(403)
reason = self.error_reason(workername, response)
self.write(f"Failed to restart the '{workername}' pool: {reason}")
class WorkerPoolGrow(ControlHandler):
@web.authenticated
def post(self, workername):
"""
Grow worker's pool
**Example request**:
.. sourcecode:: http
POST /api/worker/pool/grow/celery@worker2?n=3 HTTP/1.1
Content-Length: 0
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 58
Content-Type: application/json; charset=UTF-8
{
"message": "Growing 'celery@worker2' worker's pool by 3"
}
:query n: number of pool processes to grow, default is 1
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 403: failed to grow
:statuscode 404: unknown worker
"""
if not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
n = self.get_argument('n', default=1, type=int)
logger.info("Growing '%s' worker's pool by '%s'", workername, n)
response = self.capp.control.pool_grow(
n=n, reply=True, destination=[workername])
if response and 'ok' in response[0][workername]:
self.write(dict(message=f"Growing '{workername}' worker's pool by {n}"))
else:
logger.error(response)
self.set_status(403)
reason = self.error_reason(workername, response)
self.write(f"Failed to grow '{workername}' worker's pool: {reason}")
class WorkerPoolShrink(ControlHandler):
@web.authenticated
def post(self, workername):
"""
Shrink worker's pool
**Example request**:
.. sourcecode:: http
POST /api/worker/pool/shrink/celery@worker2 HTTP/1.1
Content-Length: 0
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 60
Content-Type: application/json; charset=UTF-8
{
"message": "Shrinking 'celery@worker2' worker's pool by 1"
}
:query n: number of pool processes to shrink, default is 1
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 403: failed to shrink
:statuscode 404: unknown worker
"""
if not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
n = self.get_argument('n', default=1, type=int)
logger.info("Shrinking '%s' worker's pool by '%s'", workername, n)
response = self.capp.control.pool_shrink(
n=n, reply=True, destination=[workername])
if response and 'ok' in response[0][workername]:
self.write(dict(message=f"Shrinking '{workername}' worker's pool by {n}"))
else:
logger.error(response)
self.set_status(403)
reason = self.error_reason(workername, response)
self.write(f"Failed to shrink '{workername}' worker's pool: {reason}")
class WorkerPoolAutoscale(ControlHandler):
@web.authenticated
def post(self, workername):
"""
Autoscale worker pool
**Example request**:
.. sourcecode:: http
POST /api/worker/pool/autoscale/celery@worker2?min=3&max=10 HTTP/1.1
Content-Length: 0
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 66
Content-Type: application/json; charset=UTF-8
{
"message": "Autoscaling 'celery@worker2' worker (min=3, max=10)"
}
:query min: minimum number of pool processes
:query max: maximum number of pool processes
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 403: autoscaling is not enabled (see CELERYD_AUTOSCALER)
:statuscode 404: unknown worker
"""
if not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
min = self.get_argument('min', type=int)
max = self.get_argument('max', type=int)
logger.info("Autoscaling '%s' worker by '%s'",
workername, (min, max))
response = self.capp.control.broadcast(
'autoscale', arguments={'min': min, 'max': max},
destination=[workername], reply=True)
if response and 'ok' in response[0][workername]:
self.write(dict(message=f"Autoscaling '{workername}' worker "
"(min={min}, max={max})"))
else:
logger.error(response)
self.set_status(403)
reason = self.error_reason(workername, response)
self.write(f"Failed to autoscale '{workername}' worker: {reason}")
class WorkerQueueAddConsumer(ControlHandler):
@web.authenticated
def post(self, workername):
"""
Start consuming from a queue
**Example request**:
.. sourcecode:: http
POST /api/worker/queue/add-consumer/celery@worker2?queue=sample-queue
Content-Length: 0
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 40
Content-Type: application/json; charset=UTF-8
{
"message": "add consumer sample-queue"
}
:query queue: the name of a new queue
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 403: failed to add consumer
:statuscode 404: unknown worker
"""
if not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
queue = self.get_argument('queue')
logger.info("Adding consumer '%s' to worker '%s'",
queue, workername)
response = self.capp.control.broadcast(
'add_consumer', arguments={'queue': queue},
destination=[workername], reply=True)
if response and 'ok' in response[0][workername]:
self.write(dict(message=response[0][workername]['ok']))
else:
logger.error(response)
self.set_status(403)
reason = self.error_reason(workername, response)
self.write(f"Failed to add '{queue}' consumer to '{workername}' worker: {reason}")
class WorkerQueueCancelConsumer(ControlHandler):
@web.authenticated
def post(self, workername):
"""
Stop consuming from a queue
**Example request**:
.. sourcecode:: http
POST /api/worker/queue/cancel-consumer/celery@worker2?queue=sample-queue
Content-Length: 0
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 52
Content-Type: application/json; charset=UTF-8
{
"message": "no longer consuming from sample-queue"
}
:query queue: the name of queue
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 403: failed to cancel consumer
:statuscode 404: unknown worker
"""
if not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
queue = self.get_argument('queue')
logger.info("Canceling consumer '%s' from worker '%s'",
queue, workername)
response = self.capp.control.broadcast(
'cancel_consumer', arguments={'queue': queue},
destination=[workername], reply=True)
if response and 'ok' in response[0][workername]:
self.write(dict(message=response[0][workername]['ok']))
else:
logger.error(response)
self.set_status(403)
reason = self.error_reason(workername, response)
self.write(f"Failed to cancel '{queue}' consumer from '{workername}' worker: {reason}")
class TaskRevoke(ControlHandler):
@web.authenticated
def post(self, taskid):
"""
Revoke a task
**Example request**:
.. sourcecode:: http
POST /api/task/revoke/1480b55c-b8b2-462c-985e-24af3e9158f9?terminate=true
Content-Length: 0
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 61
Content-Type: application/json; charset=UTF-8
{
"message": "Revoked '1480b55c-b8b2-462c-985e-24af3e9158f9'"
}
:query terminate: terminate the task if it is running
:query signal: name of signal to send to process if terminate (default: 'SIGTERM')
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
"""
logger.info("Revoking task '%s'", taskid)
terminate = self.get_argument('terminate', default=False, type=bool)
signal = self.get_argument('signal', default='SIGTERM', type=str)
self.capp.control.revoke(taskid, terminate=terminate, signal=signal)
self.write(dict(message=f"Revoked '{taskid}'"))
class TaskTimout(ControlHandler):
@web.authenticated
def post(self, taskname):
"""
Change soft and hard time limits for a task
**Example request**:
.. sourcecode:: http
POST /api/task/timeout/tasks.sleep HTTP/1.1
Content-Length: 44
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:5555
soft=30&hard=100&workername=celery%40worker1
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 46
Content-Type: application/json; charset=UTF-8
{
"message": "time limits set successfully"
}
:query workername: worker name
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 404: unknown task/worker
"""
workername = self.get_argument('workername')
hard = self.get_argument('hard', default=None, type=float)
soft = self.get_argument('soft', default=None, type=float)
if taskname not in self.capp.tasks:
raise web.HTTPError(404, f"Unknown task '{taskname}'")
if workername is not None and not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
logger.info("Setting timeouts for '%s' task (%s, %s)",
taskname, soft, hard)
destination = [workername] if workername is not None else None
response = self.capp.control.time_limit(
taskname, reply=True, hard=hard, soft=soft,
destination=destination)
if response and 'ok' in response[0][workername]:
self.write(dict(message=response[0][workername]['ok']))
else:
logger.error(response)
self.set_status(403)
reason = self.error_reason(taskname, response)
self.write(f"Failed to set timeouts: '{reason}'")
class TaskRateLimit(ControlHandler):
@web.authenticated
def post(self, taskname):
"""
Change rate limit for a task
**Example request**:
.. sourcecode:: http
POST /api/task/rate-limit/tasks.sleep HTTP/1.1
Content-Length: 41
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:5555
ratelimit=200&workername=celery%40worker1
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 61
Content-Type: application/json; charset=UTF-8
{
"message": "new rate limit set successfully"
}
:query workername: worker name
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 404: unknown task/worker
"""
workername = self.get_argument('workername')
ratelimit = self.get_argument('ratelimit')
if taskname not in self.capp.tasks:
raise web.HTTPError(404, f"Unknown task '{taskname}'")
if workername is not None and not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
logger.info("Setting '%s' rate limit for '%s' task",
ratelimit, taskname)
destination = [workername] if workername is not None else None
response = self.capp.control.rate_limit(
taskname, ratelimit, reply=True, destination=destination)
if response and 'ok' in response[0][workername]:
self.write(dict(message=response[0][workername]['ok']))
else:
logger.error(response)
self.set_status(403)
reason = self.error_reason(taskname, response)
self.write(f"Failed to set rate limit: '{reason}'")
@@ -0,0 +1,638 @@
import json
import logging
from collections import OrderedDict
from datetime import datetime
from celery import states
from celery.backends.base import DisabledBackend
from celery.contrib.abortable import AbortableAsyncResult
from celery.result import AsyncResult
from tornado import web
from tornado.escape import json_decode
from tornado.ioloop import IOLoop
from tornado.web import HTTPError
from ..utils import tasks
from ..utils.broker import Broker
from . import BaseApiHandler
logger = logging.getLogger(__name__)
class BaseTaskHandler(BaseApiHandler):
DATE_FORMAT = '%Y-%m-%d %H:%M:%S.%f'
def get_task_args(self):
try:
body = self.request.body
options = json_decode(body) if body else {}
except ValueError as e:
raise HTTPError(400, str(e)) from e
if not isinstance(options, dict):
raise HTTPError(400, 'invalid options')
args = options.pop('args', [])
kwargs = options.pop('kwargs', {})
if not isinstance(args, (list, tuple)):
raise HTTPError(400, 'args must be an array')
return args, kwargs, options
@staticmethod
def backend_configured(result):
return not isinstance(result.backend, DisabledBackend)
def write_error(self, status_code, **kwargs):
self.set_status(status_code)
def update_response_result(self, response, result):
if result.state == states.FAILURE:
response.update({'result': self.safe_result(result.result),
'traceback': result.traceback})
else:
response.update({'result': self.safe_result(result.result)})
def normalize_options(self, options):
if 'eta' in options:
options['eta'] = datetime.strptime(options['eta'],
self.DATE_FORMAT)
if 'countdown' in options:
options['countdown'] = float(options['countdown'])
if 'expires' in options:
expires = options['expires']
try:
expires = float(expires)
except ValueError:
expires = datetime.strptime(expires, self.DATE_FORMAT)
options['expires'] = expires
def safe_result(self, result):
"returns json encodable result"
try:
json.dumps(result)
except TypeError:
return repr(result)
return result
class TaskApply(BaseTaskHandler):
@web.authenticated
async def post(self, taskname):
"""
Execute a task by name and wait results
**Example request**:
.. sourcecode:: http
POST /api/task/apply/tasks.add HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate, compress
Content-Length: 16
Content-Type: application/json; charset=utf-8
Host: localhost:5555
{
"args": [1, 2]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 71
Content-Type: application/json; charset=UTF-8
{
"state": "SUCCESS",
"task-id": "c60be250-fe52-48df-befb-ac66174076e6",
"result": 3
}
:query args: a list of arguments
:query kwargs: a dictionary of arguments
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 404: unknown task
"""
args, kwargs, options = self.get_task_args()
logger.debug("Invoking a task '%s' with '%s' and '%s'",
taskname, args, kwargs)
try:
task = self.capp.tasks[taskname]
except KeyError as exc:
raise HTTPError(404, f"Unknown task '{taskname}'") from exc
try:
self.normalize_options(options)
except ValueError as exc:
raise HTTPError(400, 'Invalid option') from exc
result = task.apply_async(args=args, kwargs=kwargs, **options)
response = {'task-id': result.task_id}
response = await IOLoop.current().run_in_executor(
None, self.wait_results, result, response)
self.write(response)
def wait_results(self, result, response):
# Wait until task finished and do not raise anything
result.get(propagate=False)
# Write results and finish async function
self.update_response_result(response, result)
if self.backend_configured(result):
response.update(state=result.state)
return response
class TaskAsyncApply(BaseTaskHandler):
@web.authenticated
def post(self, taskname):
"""
Execute a task
**Example request**:
.. sourcecode:: http
POST /api/task/async-apply/tasks.add HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate, compress
Content-Length: 16
Content-Type: application/json; charset=utf-8
Host: localhost:5555
{
"args": [1, 2]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 71
Content-Type: application/json; charset=UTF-8
Date: Sun, 13 Apr 2014 15:55:00 GMT
{
"state": "PENDING",
"task-id": "abc300c7-2922-4069-97b6-a635cc2ac47c"
}
:query args: a list of arguments
:query kwargs: a dictionary of arguments
:query options: a dictionary of `apply_async` keyword arguments
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 404: unknown task
"""
args, kwargs, options = self.get_task_args()
logger.debug("Invoking a task '%s' with '%s' and '%s'",
taskname, args, kwargs)
try:
task = self.capp.tasks[taskname]
except KeyError as exc:
raise HTTPError(404, f"Unknown task '{taskname}'") from exc
try:
self.normalize_options(options)
except ValueError as exc:
raise HTTPError(400, 'Invalid option') from exc
result = task.apply_async(args=args, kwargs=kwargs, **options)
response = {'task-id': result.task_id}
if self.backend_configured(result):
response.update(state=result.state)
self.write(response)
class TaskSend(BaseTaskHandler):
@web.authenticated
def post(self, taskname):
"""
Execute a task by name (doesn't require task sources)
**Example request**:
.. sourcecode:: http
POST /api/task/send-task/tasks.add HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate, compress
Content-Length: 16
Content-Type: application/json; charset=utf-8
Host: localhost:5555
{
"args": [1, 2]
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 71
Content-Type: application/json; charset=UTF-8
{
"state": "SUCCESS",
"task-id": "c60be250-fe52-48df-befb-ac66174076e6"
}
:query args: a list of arguments
:query kwargs: a dictionary of arguments
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 404: unknown task
"""
args, kwargs, options = self.get_task_args()
logger.debug("Invoking task '%s' with '%s' and '%s'",
taskname, args, kwargs)
result = self.capp.send_task(
taskname, args=args, kwargs=kwargs, **options)
response = {'task-id': result.task_id}
if self.backend_configured(result):
response.update(state=result.state)
self.write(response)
class TaskResult(BaseTaskHandler):
@web.authenticated
def get(self, taskid):
"""
Get a task result
**Example request**:
.. sourcecode:: http
GET /api/task/result/c60be250-fe52-48df-befb-ac66174076e6 HTTP/1.1
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 84
Content-Type: application/json; charset=UTF-8
{
"result": 3,
"state": "SUCCESS",
"task-id": "c60be250-fe52-48df-befb-ac66174076e6"
}
:query timeout: how long to wait, in seconds, before the operation times out
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 503: result backend is not configured
"""
timeout = self.get_argument('timeout', None)
timeout = float(timeout) if timeout is not None else None
result = AsyncResult(taskid)
if not self.backend_configured(result):
raise HTTPError(503)
response = {'task-id': taskid, 'state': result.state}
if timeout:
result.get(timeout=timeout, propagate=False)
self.update_response_result(response, result)
elif result.ready():
self.update_response_result(response, result)
self.write(response)
class TaskAbort(BaseTaskHandler):
@web.authenticated
def post(self, taskid):
"""
Abort a running task
**Example request**:
.. sourcecode:: http
POST /api/task/abort/c60be250-fe52-48df-befb-ac66174076e6 HTTP/1.1
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 61
Content-Type: application/json; charset=UTF-8
{
"message": "Aborted '1480b55c-b8b2-462c-985e-24af3e9158f9'"
}
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 503: result backend is not configured
"""
logger.info("Aborting task '%s'", taskid)
result = AbortableAsyncResult(taskid)
if not self.backend_configured(result):
raise HTTPError(503)
result.abort()
self.write(dict(message=f"Aborted '{taskid}'"))
class GetQueueLengths(BaseTaskHandler):
@web.authenticated
async def get(self):
"""
Return length of all active queues
**Example request**:
.. sourcecode:: http
GET /api/queues/length
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 94
Content-Type: application/json; charset=UTF-8
{
"active_queues": [
{"name": "celery", "messages": 0},
{"name": "video-queue", "messages": 5}
]
}
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 503: result backend is not configured
"""
app = self.application
http_api = None
if app.transport == 'amqp' and app.options.broker_api:
http_api = app.options.broker_api
broker = Broker(app.capp.connection().as_uri(include_password=True),
http_api=http_api, broker_options=self.capp.conf.broker_transport_options,
broker_use_ssl=self.capp.conf.broker_use_ssl)
queues = await broker.queues(self.get_active_queue_names())
self.write({'active_queues': queues})
class ListTasks(BaseTaskHandler):
@web.authenticated
def get(self):
"""
List tasks
**Example request**:
.. sourcecode:: http
GET /api/tasks HTTP/1.1
Host: localhost:5555
User-Agent: HTTPie/0.8.0
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 1109
Content-Type: application/json; charset=UTF-8
Etag: "b2478118015c8b825f7b88ce6b660e5449746c37"
Server: TornadoServer/3.1.1
{
"e42ceb2d-8730-47b5-8b4d-8e0d2a1ef7c9": {
"args": "[3, 4]",
"client": null,
"clock": 1079,
"eta": null,
"exception": null,
"exchange": null,
"expires": null,
"failed": null,
"kwargs": "{}",
"name": "tasks.add",
"received": 1398505411.107885,
"result": "'7'",
"retried": null,
"retries": 0,
"revoked": null,
"routing_key": null,
"runtime": 0.01610181899741292,
"sent": null,
"started": 1398505411.108985,
"state": "SUCCESS",
"succeeded": 1398505411.124802,
"timestamp": 1398505411.124802,
"traceback": null,
"uuid": "e42ceb2d-8730-47b5-8b4d-8e0d2a1ef7c9",
"worker": "celery@worker1"
},
"f67ea225-ae9e-42a8-90b0-5de0b24507e0": {
"args": "[1, 2]",
"client": null,
"clock": 1042,
"eta": null,
"exception": null,
"exchange": null,
"expires": null,
"failed": null,
"kwargs": "{}",
"name": "tasks.add",
"received": 1398505395.327208,
"result": "'3'",
"retried": null,
"retries": 0,
"revoked": null,
"routing_key": null,
"runtime": 0.012884548006695695,
"sent": null,
"started": 1398505395.3289,
"state": "SUCCESS",
"succeeded": 1398505395.341089,
"timestamp": 1398505395.341089,
"traceback": null,
"uuid": "f67ea225-ae9e-42a8-90b0-5de0b24507e0",
"worker": "celery@worker1"
}
}
:query limit: maximum number of tasks
:query offset: skip first n tasks
:query sort_by: sort tasks by attribute (name, state, received, started)
:query workername: filter task by workername
:query taskname: filter tasks by taskname
:query state: filter tasks by state
:query received_start: filter tasks by received date (must be greater than) format %Y-%m-%d %H:%M
:query received_end: filter tasks by received date (must be less than) format %Y-%m-%d %H:%M
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
"""
app = self.application
limit = self.get_argument('limit', None)
offset = self.get_argument('offset', default=0, type=int)
worker = self.get_argument('workername', None)
type = self.get_argument('taskname', None)
state = self.get_argument('state', None)
received_start = self.get_argument('received_start', None)
received_end = self.get_argument('received_end', None)
sort_by = self.get_argument('sort_by', None)
search = self.get_argument('search', None)
limit = limit and int(limit)
offset = max(offset, 0)
worker = worker if worker != 'All' else None
type = type if type != 'All' else None
state = state if state != 'All' else None
result = []
for task_id, task in tasks.iter_tasks(
app.events, limit=limit, offset=offset, sort_by=sort_by, type=type,
worker=worker, state=state,
received_start=received_start,
received_end=received_end,
search=search
):
task = tasks.as_dict(task)
worker = task.pop('worker', None)
if worker is not None:
task['worker'] = worker.hostname
result.append((task_id, task))
self.write(OrderedDict(result))
class ListTaskTypes(BaseTaskHandler):
@web.authenticated
def get(self):
"""
List (seen) task types
**Example request**:
.. sourcecode:: http
GET /api/task/types HTTP/1.1
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 44
Content-Type: application/json; charset=UTF-8
{
"task-types": [
"tasks.add",
"tasks.sleep"
]
}
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
"""
seen_task_types = self.application.events.state.task_types()
response = {}
response['task-types'] = seen_task_types
self.write(response)
class TaskInfo(BaseTaskHandler):
@web.authenticated
def get(self, taskid):
"""
Get a task info
**Example request**:
.. sourcecode:: http
GET /api/task/info/91396550-c228-4111-9da4-9d88cfd5ddc6 HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 575
Content-Type: application/json; charset=UTF-8
{
"args": "[2, 2]",
"client": null,
"clock": 25,
"eta": null,
"exception": null,
"exchange": null,
"expires": null,
"failed": null,
"kwargs": "{}",
"name": "tasks.add",
"received": 1400806241.970742,
"result": "'4'",
"retried": null,
"retries": null,
"revoked": null,
"routing_key": null,
"runtime": 2.0037889280356467,
"sent": null,
"started": 1400806241.972624,
"state": "SUCCESS",
"succeeded": 1400806243.975336,
"task-id": "91396550-c228-4111-9da4-9d88cfd5ddc6",
"timestamp": 1400806243.975336,
"traceback": null,
"worker": "celery@worker1"
}
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
:statuscode 404: unknown task
"""
task = tasks.get_task_by_id(self.application.events, taskid)
if not task:
raise HTTPError(404, f"Unknown task '{taskid}'")
response = task.as_dict()
if task.worker is not None:
response['worker'] = task.worker.hostname
self.write(response)
@@ -0,0 +1,183 @@
import asyncio
import logging
from tornado import web
from .control import ControlHandler
logger = logging.getLogger(__name__)
class ListWorkers(ControlHandler):
@web.authenticated
async def get(self):
"""
List workers
**Example request**:
.. sourcecode:: http
GET /api/workers HTTP/1.1
Host: localhost:5555
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Length: 1526
Content-Type: application/json; charset=UTF-8
Date: Tue, 28 Jul 2015 01:32:38 GMT
Etag: "fcdd75d85a82b4052275e28871d199aac1ece21c"
Server: TornadoServer/4.0.2
{
"celery@worker1": {
"active_queues": [
{
"alias": null,
"auto_delete": false,
"binding_arguments": null,
"bindings": [],
"durable": true,
"exchange": {
"arguments": null,
"auto_delete": false,
"delivery_mode": 2,
"durable": true,
"name": "celery",
"passive": false,
"type": "direct"
},
"exclusive": false,
"name": "celery",
"no_ack": false,
"queue_arguments": null,
"routing_key": "celery"
}
],
"conf": {
"CELERYBEAT_SCHEDULE": {},
"CELERY_INCLUDE": [
"celery.app.builtins",
"__main__"
],
"CELERY_SEND_TASK_SENT_EVENT": true,
"CELERY_TIMEZONE": "UTC"
},
"registered": [
"tasks.add",
"tasks.echo",
"tasks.error",
"tasks.retry",
"tasks.sleep"
],
"stats": {
"broker": {
"alternates": [],
"connect_timeout": 4,
"heartbeat": null,
"hostname": "127.0.0.1",
"insist": false,
"login_method": "AMQPLAIN",
"port": 5672,
"ssl": false,
"transport": "amqp",
"transport_options": {},
"uri_prefix": null,
"userid": "guest",
"virtual_host": "/"
},
"clock": "918",
"pid": 90494,
"pool": {
"max-concurrency": 4,
"max-tasks-per-child": "N/A",
"processes": [
90499,
90500,
90501,
90502
],
"put-guarded-by-semaphore": false,
"timeouts": [
0,
0
],
"writes": {
"all": "100.00%",
"avg": "100.00%",
"inqueues": {
"active": 0,
"total": 4
},
"raw": "1",
"total": 1
}
},
"prefetch_count": 16,
"rusage": {
"idrss": 0,
"inblock": 211,
"isrss": 0,
"ixrss": 0,
"majflt": 6,
"maxrss": 26996736,
"minflt": 11450,
"msgrcv": 4968,
"msgsnd": 1227,
"nivcsw": 1367,
"nsignals": 0,
"nswap": 0,
"nvcsw": 1855,
"oublock": 93,
"stime": 0.414564,
"utime": 0.975726
},
"total": {
"tasks.add": 1
}
},
"timestamp": 1438049312.073402
}
}
:query refresh: run inspect to get updated list of workers
:query workername: get info for workername
:query status: only get worker status info
:reqheader Authorization: optional OAuth token to authenticate
:statuscode 200: no error
:statuscode 401: unauthorized request
"""
refresh = self.get_argument('refresh', default=False, type=bool)
status = self.get_argument('status', default=False, type=bool)
workername = self.get_argument('workername', default=None)
if refresh:
try:
await asyncio.wait(self.application.update_workers(workername=workername))
except Exception as e:
msg = f"Failed to update workers: {e}"
logger.error(msg)
raise web.HTTPError(503, msg)
if status:
info = {}
for name, worker in self.application.events.state.workers.items():
info[name] = worker.alive
self.write(info)
return
if self.application.workers and not refresh and\
workername in self.application.workers:
self.write({workername: self.application.workers[workername]})
return
if workername and not self.is_worker(workername):
raise web.HTTPError(404, f"Unknown worker '{workername}'")
if workername:
self.write({workername: self.application.workers[workername]})
else:
self.write(self.application.workers)