Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F9345811
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
28 KB
Subscribers
None
View Options
diff --git a/swh/scheduler/api/client.py b/swh/scheduler/api/client.py
index 7c07cc4..1d84894 100644
--- a/swh/scheduler/api/client.py
+++ b/swh/scheduler/api/client.py
@@ -1,106 +1,109 @@
# Copyright (C) 2018 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
from swh.core.api import RPCClient
class RemoteScheduler(RPCClient):
"""Proxy to a remote scheduler API
"""
def close_connection(self):
return self.post('close_connection', {})
def set_status_tasks(self, task_ids, status='disabled', next_run=None):
return self.post('set_status_tasks', dict(
task_ids=task_ids, status=status, next_run=next_run))
def create_task_type(self, task_type):
return self.post('create_task_type', {'task_type': task_type})
def get_task_type(self, task_type_name):
return self.post('get_task_type', {'task_type_name': task_type_name})
def get_task_types(self):
return self.post('get_task_types', {})
def create_tasks(self, tasks):
return self.post('create_tasks', {'tasks': tasks})
def disable_tasks(self, task_ids):
return self.post('disable_tasks', {'task_ids': task_ids})
def get_tasks(self, task_ids):
return self.post('get_tasks', {'task_ids': task_ids})
def get_task_runs(self, task_ids, limit=None):
return self.post(
'get_task_runs', {'task_ids': task_ids, 'limit': limit})
def search_tasks(self, task_id=None, task_type=None, status=None,
priority=None, policy=None, before=None, after=None,
limit=None):
return self.post('search_tasks', dict(
task_id=task_id, task_type=task_type, status=status,
priority=priority, policy=policy, before=before, after=after,
limit=limit))
def peek_ready_tasks(self, task_type, timestamp=None, num_tasks=None,
num_tasks_priority=None):
return self.post('peek_ready_tasks', {
'task_type': task_type,
'timestamp': timestamp,
'num_tasks': num_tasks,
'num_tasks_priority': num_tasks_priority,
})
def grab_ready_tasks(self, task_type, timestamp=None, num_tasks=None,
num_tasks_priority=None):
return self.post('grab_ready_tasks', {
'task_type': task_type,
'timestamp': timestamp,
'num_tasks': num_tasks,
'num_tasks_priority': num_tasks_priority,
})
def schedule_task_run(self, task_id, backend_id, metadata=None,
timestamp=None):
return self.post('schedule_task_run', {
'task_id': task_id,
'backend_id': backend_id,
'metadata': metadata,
'timestamp': timestamp,
})
def mass_schedule_task_runs(self, task_runs):
return self.post('mass_schedule_task_runs', {'task_runs': task_runs})
def start_task_run(self, backend_id, metadata=None, timestamp=None):
return self.post('start_task_run', {
'backend_id': backend_id,
'metadata': metadata,
'timestamp': timestamp,
})
def end_task_run(self, backend_id, status, metadata=None, timestamp=None):
return self.post('end_task_run', {
'backend_id': backend_id,
'status': status,
'metadata': metadata,
'timestamp': timestamp,
})
def filter_task_to_archive(self, after_ts, before_ts, limit=10,
last_id=-1):
return self.post('filter_task_to_archive', {
'after_ts': after_ts,
'before_ts': before_ts,
'limit': limit,
'last_id': last_id,
})
def delete_archived_tasks(self, task_ids):
return self.post('delete_archived_tasks', {'task_ids': task_ids})
+
+ def get_priority_ratios(self):
+ return self.get('get_priority_ratios')
diff --git a/swh/scheduler/api/server.py b/swh/scheduler/api/server.py
index 3cdb28d..8f243be 100644
--- a/swh/scheduler/api/server.py
+++ b/swh/scheduler/api/server.py
@@ -1,259 +1,266 @@
# Copyright (C) 2018-2019 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
import os
import logging
from flask import request, Flask
from swh.core import config
from swh.core.api import (decode_request,
error_handler,
encode_data_server as encode_data)
from swh.core.api import negotiate, JSONFormatter, MsgpackFormatter
from swh.scheduler import get_scheduler as get_scheduler_from
app = Flask(__name__)
scheduler = None
@app.errorhandler(Exception)
def my_error_handler(exception):
return error_handler(exception, encode_data)
def get_sched():
global scheduler
if not scheduler:
scheduler = get_scheduler_from(**app.config['scheduler'])
return scheduler
def has_no_empty_params(rule):
return len(rule.defaults or ()) >= len(rule.arguments or ())
@app.route('/')
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def index():
return 'SWH Scheduler API server'
@app.route('/close_connection', methods=['GET', 'POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def close_connection():
return get_sched().close_connection()
@app.route('/set_status_tasks', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def set_status_tasks():
return get_sched().set_status_tasks(**decode_request(request))
@app.route('/create_task_type', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def create_task_type():
return get_sched().create_task_type(**decode_request(request))
@app.route('/get_task_type', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def get_task_type():
return get_sched().get_task_type(**decode_request(request))
@app.route('/get_task_types', methods=['GET', 'POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def get_task_types():
return get_sched().get_task_types(**decode_request(request))
@app.route('/create_tasks', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def create_tasks():
return get_sched().create_tasks(**decode_request(request))
@app.route('/disable_tasks', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def disable_tasks():
return get_sched().disable_tasks(**decode_request(request))
@app.route('/get_tasks', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def get_tasks():
return get_sched().get_tasks(**decode_request(request))
@app.route('/get_task_runs', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def get_task_runs():
return get_sched().get_task_runs(**decode_request(request))
@app.route('/search_tasks', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def search_tasks():
return get_sched().search_tasks(**decode_request(request))
@app.route('/peek_ready_tasks', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def peek_ready_tasks():
return get_sched().peek_ready_tasks(**decode_request(request))
@app.route('/grab_ready_tasks', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def grab_ready_tasks():
return get_sched().grab_ready_tasks(**decode_request(request))
@app.route('/schedule_task_run', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def schedule_task_run():
return get_sched().schedule_task_run(**decode_request(request))
@app.route('/mass_schedule_task_runs', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def mass_schedule_task_runs():
return get_sched().mass_schedule_task_runs(**decode_request(request))
@app.route('/start_task_run', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def start_task_run():
return get_sched().start_task_run(**decode_request(request))
@app.route('/end_task_run', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def end_task_run():
return get_sched().end_task_run(**decode_request(request))
@app.route('/filter_task_to_archive', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def filter_task_to_archive():
return get_sched().filter_task_to_archive(**decode_request(request))
@app.route('/delete_archived_tasks', methods=['POST'])
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def delete_archived_tasks():
return get_sched().delete_archived_tasks(**decode_request(request))
+@app.route('/get_priority_ratios', methods=['GET', 'POST'])
+@negotiate(MsgpackFormatter)
+@negotiate(JSONFormatter)
+def get_priority_ratios():
+ return get_sched().get_priority_ratios(**decode_request(request))
+
+
@app.route("/site-map")
@negotiate(MsgpackFormatter)
@negotiate(JSONFormatter)
def site_map():
links = []
sched = get_sched()
for rule in app.url_map.iter_rules():
if has_no_empty_params(rule) and hasattr(sched, rule.endpoint):
links.append(dict(
rule=rule.rule,
description=getattr(sched, rule.endpoint).__doc__))
# links is now a list of url, endpoint tuples
return links
def load_and_check_config(config_file, type='local'):
"""Check the minimal configuration is set to run the api or raise an
error explanation.
Args:
config_file (str): Path to the configuration file to load
type (str): configuration type. For 'local' type, more
checks are done.
Raises:
Error if the setup is not as expected
Returns:
configuration as a dict
"""
if not config_file:
raise EnvironmentError('Configuration file must be defined')
if not os.path.exists(config_file):
raise FileNotFoundError('Configuration file %s does not exist' % (
config_file, ))
cfg = config.read(config_file)
vcfg = cfg.get('scheduler')
if not vcfg:
raise KeyError("Missing '%scheduler' configuration")
if type == 'local':
cls = vcfg.get('cls')
if cls != 'local':
raise ValueError(
"The scheduler backend can only be started with a 'local' "
"configuration")
args = vcfg.get('args')
if not args:
raise KeyError(
"Invalid configuration; missing 'args' config entry")
db = args.get('db')
if not db:
raise KeyError(
"Invalid configuration; missing 'db' config entry")
return cfg
api_cfg = None
def make_app_from_configfile():
"""Run the WSGI app from the webserver, loading the configuration from
a configuration file.
SWH_CONFIG_FILENAME environment variable defines the
configuration path to load.
"""
global api_cfg
if not api_cfg:
config_file = os.environ.get('SWH_CONFIG_FILENAME')
api_cfg = load_and_check_config(config_file)
app.config.update(api_cfg)
handler = logging.StreamHandler()
app.logger.addHandler(handler)
return app
if __name__ == '__main__':
print('Please use the "swh-scheduler api-server" command')
diff --git a/swh/scheduler/backend.py b/swh/scheduler/backend.py
index 4a042d1..f57ca46 100644
--- a/swh/scheduler/backend.py
+++ b/swh/scheduler/backend.py
@@ -1,488 +1,493 @@
# Copyright (C) 2015-2018 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information
import json
import logging
from arrow import Arrow, utcnow
import psycopg2.pool
import psycopg2.extras
from psycopg2.extensions import AsIs
from swh.core.db import BaseDb
from swh.core.db.common import db_transaction, db_transaction_generator
logger = logging.getLogger(__name__)
def adapt_arrow(arrow):
return AsIs("'%s'::timestamptz" % arrow.isoformat())
psycopg2.extensions.register_adapter(dict, psycopg2.extras.Json)
psycopg2.extensions.register_adapter(Arrow, adapt_arrow)
def format_query(query, keys):
"""Format a query with the given keys"""
query_keys = ', '.join(keys)
placeholders = ', '.join(['%s'] * len(keys))
return query.format(keys=query_keys, placeholders=placeholders)
class SchedulerBackend:
"""Backend for the Software Heritage scheduling database.
"""
def __init__(self, db, min_pool_conns=1, max_pool_conns=10):
"""
Args:
db_conn: either a libpq connection string, or a psycopg2 connection
"""
if isinstance(db, psycopg2.extensions.connection):
self._pool = None
self._db = BaseDb(db)
else:
self._pool = psycopg2.pool.ThreadedConnectionPool(
min_pool_conns, max_pool_conns, db,
cursor_factory=psycopg2.extras.RealDictCursor,
)
self._db = None
def get_db(self):
if self._db:
return self._db
return BaseDb.from_pool(self._pool)
def put_db(self, db):
if db is not self._db:
db.put_conn()
task_type_keys = [
'type', 'description', 'backend_name', 'default_interval',
'min_interval', 'max_interval', 'backoff_factor', 'max_queue_length',
'num_retries', 'retry_delay',
]
@db_transaction()
def create_task_type(self, task_type, db=None, cur=None):
"""Create a new task type ready for scheduling.
Args:
task_type (dict): a dictionary with the following keys:
- type (str): an identifier for the task type
- description (str): a human-readable description of what the
task does
- backend_name (str): the name of the task in the
job-scheduling backend
- default_interval (datetime.timedelta): the default interval
between two task runs
- min_interval (datetime.timedelta): the minimum interval
between two task runs
- max_interval (datetime.timedelta): the maximum interval
between two task runs
- backoff_factor (float): the factor by which the interval
changes at each run
- max_queue_length (int): the maximum length of the task queue
for this task type
"""
keys = [key for key in self.task_type_keys if key in task_type]
query = format_query(
"""insert into task_type ({keys}) values ({placeholders})""",
keys)
cur.execute(query, [task_type[key] for key in keys])
@db_transaction()
def get_task_type(self, task_type_name, db=None, cur=None):
"""Retrieve the task type with id task_type_name"""
query = format_query(
"select {keys} from task_type where type=%s",
self.task_type_keys,
)
cur.execute(query, (task_type_name,))
return cur.fetchone()
@db_transaction()
def get_task_types(self, db=None, cur=None):
"""Retrieve all registered task types"""
query = format_query(
"select {keys} from task_type",
self.task_type_keys,
)
cur.execute(query)
return cur.fetchall()
task_create_keys = [
'type', 'arguments', 'next_run', 'policy', 'status', 'retries_left',
'priority'
]
task_keys = task_create_keys + ['id', 'current_interval']
@db_transaction()
def create_tasks(self, tasks, policy='recurring', db=None, cur=None):
"""Create new tasks.
Args:
tasks (list): each task is a dictionary with the following keys:
- type (str): the task type
- arguments (dict): the arguments for the task runner, keys:
- args (list of str): arguments
- kwargs (dict str -> str): keyword arguments
- next_run (datetime.datetime): the next scheduled run for the
task
Returns:
a list of created tasks.
"""
cur.execute('select swh_scheduler_mktemp_task()')
db.copy_to(tasks, 'tmp_task', self.task_create_keys,
default_values={
'policy': policy,
'status': 'next_run_not_scheduled'
},
cur=cur)
query = format_query(
'select {keys} from swh_scheduler_create_tasks_from_temp()',
self.task_keys,
)
cur.execute(query)
return cur.fetchall()
@db_transaction()
def set_status_tasks(self, task_ids, status='disabled', next_run=None,
db=None, cur=None):
"""Set the tasks' status whose ids are listed.
If given, also set the next_run date.
"""
if not task_ids:
return
query = ["UPDATE task SET status = %s"]
args = [status]
if next_run:
query.append(', next_run = %s')
args.append(next_run)
query.append(" WHERE id IN %s")
args.append(tuple(task_ids))
cur.execute(''.join(query), args)
@db_transaction()
def disable_tasks(self, task_ids, db=None, cur=None):
"""Disable the tasks whose ids are listed."""
return self.set_status_tasks(task_ids, db=db, cur=cur)
@db_transaction()
def search_tasks(self, task_id=None, task_type=None, status=None,
priority=None, policy=None, before=None, after=None,
limit=None, db=None, cur=None):
"""Search tasks from selected criterions"""
where = []
args = []
if task_id:
if isinstance(task_id, (str, int)):
where.append('id = %s')
else:
where.append('id in %s')
task_id = tuple(task_id)
args.append(task_id)
if task_type:
if isinstance(task_type, str):
where.append('type = %s')
else:
where.append('type in %s')
task_type = tuple(task_type)
args.append(task_type)
if status:
if isinstance(status, str):
where.append('status = %s')
else:
where.append('status in %s')
status = tuple(status)
args.append(status)
if priority:
if isinstance(priority, str):
where.append('priority = %s')
else:
priority = tuple(priority)
where.append('priority in %s')
args.append(priority)
if policy:
where.append('policy = %s')
args.append(policy)
if before:
where.append('next_run <= %s')
args.append(before)
if after:
where.append('next_run >= %s')
args.append(after)
query = 'select * from task'
if where:
query += ' where ' + ' and '.join(where)
if limit:
query += ' limit %s :: bigint'
args.append(limit)
cur.execute(query, args)
return cur.fetchall()
@db_transaction()
def get_tasks(self, task_ids, db=None, cur=None):
"""Retrieve the info of tasks whose ids are listed."""
query = format_query('select {keys} from task where id in %s',
self.task_keys)
cur.execute(query, (tuple(task_ids),))
return cur.fetchall()
@db_transaction()
def peek_ready_tasks(self, task_type, timestamp=None, num_tasks=None,
num_tasks_priority=None,
db=None, cur=None):
"""Fetch the list of ready tasks
Args:
task_type (str): filtering task per their type
timestamp (datetime.datetime): peek tasks that need to be executed
before that timestamp
num_tasks (int): only peek at num_tasks tasks (with no priority)
num_tasks_priority (int): only peek at num_tasks_priority
tasks (with priority)
Returns:
a list of tasks
"""
if timestamp is None:
timestamp = utcnow()
cur.execute(
'''select * from swh_scheduler_peek_ready_tasks(
%s, %s, %s :: bigint, %s :: bigint)''',
(task_type, timestamp, num_tasks, num_tasks_priority)
)
logger.debug('PEEK %s => %s' % (task_type, cur.rowcount))
return cur.fetchall()
@db_transaction()
def grab_ready_tasks(self, task_type, timestamp=None, num_tasks=None,
num_tasks_priority=None, db=None, cur=None):
"""Fetch the list of ready tasks, and mark them as scheduled
Args:
task_type (str): filtering task per their type
timestamp (datetime.datetime): grab tasks that need to be executed
before that timestamp
num_tasks (int): only grab num_tasks tasks (with no priority)
num_tasks_priority (int): only grab oneshot num_tasks tasks (with
priorities)
Returns:
a list of tasks
"""
if timestamp is None:
timestamp = utcnow()
cur.execute(
'''select * from swh_scheduler_grab_ready_tasks(
%s, %s, %s :: bigint, %s :: bigint)''',
(task_type, timestamp, num_tasks, num_tasks_priority)
)
logger.debug('GRAB %s => %s' % (task_type, cur.rowcount))
return cur.fetchall()
task_run_create_keys = ['task', 'backend_id', 'scheduled', 'metadata']
@db_transaction()
def schedule_task_run(self, task_id, backend_id, metadata=None,
timestamp=None, db=None, cur=None):
"""Mark a given task as scheduled, adding a task_run entry in the database.
Args:
task_id (int): the identifier for the task being scheduled
backend_id (str): the identifier of the job in the backend
metadata (dict): metadata to add to the task_run entry
timestamp (datetime.datetime): the instant the event occurred
Returns:
a fresh task_run entry
"""
if metadata is None:
metadata = {}
if timestamp is None:
timestamp = utcnow()
cur.execute(
'select * from swh_scheduler_schedule_task_run(%s, %s, %s, %s)',
(task_id, backend_id, metadata, timestamp)
)
return cur.fetchone()
@db_transaction()
def mass_schedule_task_runs(self, task_runs, db=None, cur=None):
"""Schedule a bunch of task runs.
Args:
task_runs (list): a list of dicts with keys:
- task (int): the identifier for the task being scheduled
- backend_id (str): the identifier of the job in the backend
- metadata (dict): metadata to add to the task_run entry
- scheduled (datetime.datetime): the instant the event occurred
Returns:
None
"""
cur.execute('select swh_scheduler_mktemp_task_run()')
db.copy_to(task_runs, 'tmp_task_run', self.task_run_create_keys,
cur=cur)
cur.execute('select swh_scheduler_schedule_task_run_from_temp()')
@db_transaction()
def start_task_run(self, backend_id, metadata=None, timestamp=None,
db=None, cur=None):
"""Mark a given task as started, updating the corresponding task_run
entry in the database.
Args:
backend_id (str): the identifier of the job in the backend
metadata (dict): metadata to add to the task_run entry
timestamp (datetime.datetime): the instant the event occurred
Returns:
the updated task_run entry
"""
if metadata is None:
metadata = {}
if timestamp is None:
timestamp = utcnow()
cur.execute(
'select * from swh_scheduler_start_task_run(%s, %s, %s)',
(backend_id, metadata, timestamp)
)
return cur.fetchone()
@db_transaction()
def end_task_run(self, backend_id, status, metadata=None, timestamp=None,
result=None, db=None, cur=None):
"""Mark a given task as ended, updating the corresponding task_run entry in the
database.
Args:
backend_id (str): the identifier of the job in the backend
status (str): how the task ended; one of: 'eventful', 'uneventful',
'failed'
metadata (dict): metadata to add to the task_run entry
timestamp (datetime.datetime): the instant the event occurred
Returns:
the updated task_run entry
"""
if metadata is None:
metadata = {}
if timestamp is None:
timestamp = utcnow()
cur.execute(
'select * from swh_scheduler_end_task_run(%s, %s, %s, %s)',
(backend_id, status, metadata, timestamp)
)
return cur.fetchone()
@db_transaction_generator()
def filter_task_to_archive(self, after_ts, before_ts, limit=10, last_id=-1,
db=None, cur=None):
"""Returns the list of task/task_run prior to a given date to archive.
"""
last_task_run_id = None
while True:
row = None
cur.execute(
"select * from swh_scheduler_task_to_archive(%s, %s, %s, %s)",
(after_ts, before_ts, last_id, limit)
)
for row in cur:
# nested type index does not accept bare values
# transform it as a dict to comply with this
row['arguments']['args'] = {
i: v for i, v in enumerate(row['arguments']['args'])
}
kwargs = row['arguments']['kwargs']
row['arguments']['kwargs'] = json.dumps(kwargs)
yield row
if not row:
break
_id = row.get('task_id')
_task_run_id = row.get('task_run_id')
if last_id == _id and last_task_run_id == _task_run_id:
break
last_id = _id
last_task_run_id = _task_run_id
@db_transaction()
def delete_archived_tasks(self, task_ids, db=None, cur=None):
"""Delete archived tasks as much as possible. Only the task_ids whose
complete associated task_run have been cleaned up will be.
"""
_task_ids = _task_run_ids = []
for task_id in task_ids:
_task_ids.append(task_id['task_id'])
_task_run_ids.append(task_id['task_run_id'])
cur.execute(
"select * from swh_scheduler_delete_archived_tasks(%s, %s)",
(_task_ids, _task_run_ids))
task_run_keys = ['id', 'task', 'backend_id', 'scheduled',
'started', 'ended', 'metadata', 'status', ]
@db_transaction()
def get_task_runs(self, task_ids, limit=None, db=None, cur=None):
"""Search task run for a task id"""
where = []
args = []
if task_ids:
if isinstance(task_ids, (str, int)):
where.append('task = %s')
else:
where.append('task in %s')
task_ids = tuple(task_ids)
args.append(task_ids)
else:
return ()
query = 'select * from task_run where ' + ' and '.join(where)
if limit:
query += ' limit %s :: bigint'
args.append(limit)
cur.execute(query, args)
return cur.fetchall()
+
+ @db_transaction()
+ def get_priority_ratios(self, db=None, cur=None):
+ cur.execute('select id, ratio from priority_ratio')
+ return {row['id']: row['ratio'] for row in cur.fetchall()}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Jul 4, 3:32 PM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3251033
Attached To
rDSCH Scheduling utilities
Event Timeline
Log In to Comment