Merge pull request #1320 from niphlod/enhancement/redis_scheduler
sync with main scheduler
This commit is contained in:
@@ -9,6 +9,17 @@ Scheduler with redis backend
|
|||||||
---------------------------------
|
---------------------------------
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from gluon.utils import web2py_uuid
|
||||||
|
from gluon.storage import Storage
|
||||||
|
from gluon.scheduler import *
|
||||||
|
from gluon.scheduler import _decode_dict
|
||||||
|
from gluon.contrib.redis_utils import RWatchError
|
||||||
|
|
||||||
USAGE = """
|
USAGE = """
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
@@ -35,11 +46,6 @@ mysched = RScheduler(db, dict(demo1=demo1,demo2=demo2), ...., redis_conn=rconn)
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import socket
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
|
|
||||||
path = os.getcwd()
|
path = os.getcwd()
|
||||||
|
|
||||||
@@ -47,20 +53,19 @@ if 'WEB2PY_PATH' not in os.environ:
|
|||||||
os.environ['WEB2PY_PATH'] = path
|
os.environ['WEB2PY_PATH'] = path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from gluon.contrib.simplejson import loads, dumps
|
# try external module
|
||||||
except:
|
|
||||||
from simplejson import loads, dumps
|
from simplejson import loads, dumps
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# try stdlib (Python >= 2.6)
|
||||||
|
from json import loads, dumps
|
||||||
|
except:
|
||||||
|
# fallback to pure-Python module
|
||||||
|
from gluon.contrib.simplejson import loads, dumps
|
||||||
|
|
||||||
IDENTIFIER = "%s#%s" % (socket.gethostname(), os.getpid())
|
IDENTIFIER = "%s#%s" % (socket.gethostname(), os.getpid())
|
||||||
|
|
||||||
logger = logging.getLogger('web2py.rscheduler.%s' % IDENTIFIER)
|
logger = logging.getLogger('web2py.scheduler.%s' % IDENTIFIER)
|
||||||
|
|
||||||
from gluon.utils import web2py_uuid
|
|
||||||
from gluon.storage import Storage
|
|
||||||
from gluon.scheduler import *
|
|
||||||
from gluon.scheduler import _decode_dict
|
|
||||||
from gluon.contrib.redis_utils import RWatchError
|
|
||||||
|
|
||||||
|
|
||||||
POLLING = 'POLLING'
|
POLLING = 'POLLING'
|
||||||
|
|
||||||
@@ -111,8 +116,7 @@ class RScheduler(Scheduler):
|
|||||||
self._application = current.request.application or 'appname'
|
self._application = current.request.application or 'appname'
|
||||||
|
|
||||||
def _nkey(self, key):
|
def _nkey(self, key):
|
||||||
"""Helper to restrict all keys to a namespace
|
"""Helper to restrict all keys to a namespace and track them."""
|
||||||
and track them"""
|
|
||||||
prefix = 'w2p:rsched:%s' % self._application
|
prefix = 'w2p:rsched:%s' % self._application
|
||||||
allkeys = '%s:allkeys' % prefix
|
allkeys = '%s:allkeys' % prefix
|
||||||
newkey = "%s:%s" % (prefix, key)
|
newkey = "%s:%s" % (prefix, key)
|
||||||
@@ -120,10 +124,7 @@ class RScheduler(Scheduler):
|
|||||||
return newkey
|
return newkey
|
||||||
|
|
||||||
def prune_all(self):
|
def prune_all(self):
|
||||||
"""
|
"""Global housekeeping."""
|
||||||
Just to be fair and implement a method
|
|
||||||
that does housekeeping
|
|
||||||
"""
|
|
||||||
all_keys = self._nkey('allkeys')
|
all_keys = self._nkey('allkeys')
|
||||||
with self.r_server.pipeline() as pipe:
|
with self.r_server.pipeline() as pipe:
|
||||||
while True:
|
while True:
|
||||||
@@ -148,8 +149,9 @@ class RScheduler(Scheduler):
|
|||||||
|
|
||||||
def send_heartbeat(self, counter):
|
def send_heartbeat(self, counter):
|
||||||
"""
|
"""
|
||||||
workers coordination has evolved into something is not that
|
Workers coordination in redis.
|
||||||
easy. Here we try to do what we need in a single transaction,
|
It has evolved into something is not that easy.
|
||||||
|
Here we try to do what we need in a single transaction,
|
||||||
and retry that transaction if something goes wrong
|
and retry that transaction if something goes wrong
|
||||||
"""
|
"""
|
||||||
with self.r_server.pipeline() as pipe:
|
with self.r_server.pipeline() as pipe:
|
||||||
@@ -167,7 +169,9 @@ class RScheduler(Scheduler):
|
|||||||
|
|
||||||
def inner_send_heartbeat(self, counter, pipe):
|
def inner_send_heartbeat(self, counter, pipe):
|
||||||
"""
|
"""
|
||||||
Does a few things:
|
Do a few things in the "maintenance" thread.
|
||||||
|
|
||||||
|
Specifically:
|
||||||
- registers the workers
|
- registers the workers
|
||||||
- accepts commands sent to workers (KILL, TERMINATE, PICK, DISABLED, etc)
|
- accepts commands sent to workers (KILL, TERMINATE, PICK, DISABLED, etc)
|
||||||
- adjusts sleep
|
- adjusts sleep
|
||||||
@@ -269,6 +273,8 @@ class RScheduler(Scheduler):
|
|||||||
|
|
||||||
def being_a_ticker(self, pipe):
|
def being_a_ticker(self, pipe):
|
||||||
"""
|
"""
|
||||||
|
Elects a ticker.
|
||||||
|
|
||||||
This is slightly more convoluted than the original
|
This is slightly more convoluted than the original
|
||||||
but if far more efficient
|
but if far more efficient
|
||||||
"""
|
"""
|
||||||
@@ -311,7 +317,9 @@ class RScheduler(Scheduler):
|
|||||||
|
|
||||||
def assign_tasks(self, db):
|
def assign_tasks(self, db):
|
||||||
"""
|
"""
|
||||||
The real beauty. We don't need to ASSIGN tasks, we just put
|
The real beauty.
|
||||||
|
|
||||||
|
We don't need to ASSIGN tasks, we just put
|
||||||
them into the relevant queue
|
them into the relevant queue
|
||||||
"""
|
"""
|
||||||
st, sd = db.scheduler_task, db.scheduler_task_deps
|
st, sd = db.scheduler_task, db.scheduler_task_deps
|
||||||
@@ -375,9 +383,6 @@ class RScheduler(Scheduler):
|
|||||||
|
|
||||||
all_available = db(
|
all_available = db(
|
||||||
(st.status.belongs((QUEUED, ASSIGNED))) &
|
(st.status.belongs((QUEUED, ASSIGNED))) &
|
||||||
((st.times_run < st.repeats) | (st.repeats == 0)) &
|
|
||||||
(st.start_time <= now) &
|
|
||||||
((st.stop_time == None) | (st.stop_time > now)) &
|
|
||||||
(st.next_run_time <= now) &
|
(st.next_run_time <= now) &
|
||||||
(st.enabled == True) &
|
(st.enabled == True) &
|
||||||
(st.id.belongs(no_deps))
|
(st.id.belongs(no_deps))
|
||||||
@@ -437,6 +442,7 @@ class RScheduler(Scheduler):
|
|||||||
logger.info('TICKER: tasks are %s', x)
|
logger.info('TICKER: tasks are %s', x)
|
||||||
|
|
||||||
def pop_task(self, db):
|
def pop_task(self, db):
|
||||||
|
"""Lift a task off a queue."""
|
||||||
r_server = self.r_server
|
r_server = self.r_server
|
||||||
st = self.db.scheduler_task
|
st = self.db.scheduler_task
|
||||||
task = None
|
task = None
|
||||||
@@ -533,7 +539,9 @@ class RScheduler(Scheduler):
|
|||||||
|
|
||||||
def report_task(self, task, task_report):
|
def report_task(self, task, task_report):
|
||||||
"""
|
"""
|
||||||
Needs overwriting only because we need to pop from the
|
Override.
|
||||||
|
|
||||||
|
Needs it only because we need to pop from the
|
||||||
running tasks
|
running tasks
|
||||||
"""
|
"""
|
||||||
r_server = self.r_server
|
r_server = self.r_server
|
||||||
@@ -558,12 +566,12 @@ class RScheduler(Scheduler):
|
|||||||
logger.debug(' deleting task report in db because of no result')
|
logger.debug(' deleting task report in db because of no result')
|
||||||
db(sr.id == task.run_id).delete()
|
db(sr.id == task.run_id).delete()
|
||||||
# if there is a stop_time and the following run would exceed it
|
# if there is a stop_time and the following run would exceed it
|
||||||
is_expired = (task.stop_time
|
is_expired = (task.stop_time and
|
||||||
and task.next_run_time > task.stop_time
|
task.next_run_time > task.stop_time and
|
||||||
and True or False)
|
True or False)
|
||||||
status = (task.run_again and is_expired and EXPIRED
|
status = (task.run_again and is_expired and EXPIRED or
|
||||||
or task.run_again and not is_expired
|
task.run_again and not is_expired and
|
||||||
and QUEUED or COMPLETED)
|
QUEUED or COMPLETED)
|
||||||
if task_report.status == COMPLETED:
|
if task_report.status == COMPLETED:
|
||||||
# assigned calculations
|
# assigned calculations
|
||||||
d = dict(status=status,
|
d = dict(status=status,
|
||||||
@@ -579,10 +587,10 @@ class RScheduler(Scheduler):
|
|||||||
st_mapping = {'FAILED': 'FAILED',
|
st_mapping = {'FAILED': 'FAILED',
|
||||||
'TIMEOUT': 'TIMEOUT',
|
'TIMEOUT': 'TIMEOUT',
|
||||||
'STOPPED': 'FAILED'}[task_report.status]
|
'STOPPED': 'FAILED'}[task_report.status]
|
||||||
status = (task.retry_failed
|
status = (task.retry_failed and
|
||||||
and task.times_failed < task.retry_failed
|
task.times_failed < task.retry_failed and
|
||||||
and QUEUED or task.retry_failed == -1
|
QUEUED or task.retry_failed == -1 and
|
||||||
and QUEUED or st_mapping)
|
QUEUED or st_mapping)
|
||||||
db(st.id == task.task_id).update(
|
db(st.id == task.task_id).update(
|
||||||
times_failed=st.times_failed + 1,
|
times_failed=st.times_failed + 1,
|
||||||
next_run_time=task.next_run_time,
|
next_run_time=task.next_run_time,
|
||||||
@@ -596,7 +604,7 @@ class RScheduler(Scheduler):
|
|||||||
r_server.hdel(running_dict, task.task_id)
|
r_server.hdel(running_dict, task.task_id)
|
||||||
|
|
||||||
def wrapped_pop_task(self):
|
def wrapped_pop_task(self):
|
||||||
"""Commodity function to call `pop_task` and trap exceptions
|
"""Commodity function to call `pop_task` and trap exceptions.
|
||||||
If an exception is raised, assume it happened because of database
|
If an exception is raised, assume it happened because of database
|
||||||
contention and retries `pop_task` after 0.5 seconds
|
contention and retries `pop_task` after 0.5 seconds
|
||||||
"""
|
"""
|
||||||
@@ -620,8 +628,8 @@ class RScheduler(Scheduler):
|
|||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
def get_workers(self, only_ticker=False):
|
def get_workers(self, only_ticker=False):
|
||||||
""" Returns a dict holding worker_name : {**columns}
|
"""Return a dict holding worker_name : {**columns}
|
||||||
representing all "registered" workers
|
representing all "registered" workers.
|
||||||
only_ticker returns only the worker running as a TICKER,
|
only_ticker returns only the worker running as a TICKER,
|
||||||
if there is any
|
if there is any
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user