Changeset 184
- Timestamp:
- 09/20/10 17:18:03
- Files:
-
- futility/__init__.py (modified) (10 diffs)
- futility/root.py (modified) (8 diffs)
- futility/task.html (modified) (1 diff)
- futility/tasks.html (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
futility/__init__.py
r183 r184 11 11 from sqlalchemy.types import Integer, String, DateTime, Boolean 12 12 from sqlalchemy.orm import relation 13 from sqlalchemy.sql import and_ 13 from sqlalchemy.sql import and_, desc 14 14 # If you want to use threadlocals, import sqlalchemy.mods.threadlocal before you 15 15 # import this module, so that the following line imports the threadlocal mapper: … … 20 20 increment_test[0] = increment_test[0] + amt 21 21 task_executors = {'Increment Test': _incr} 22 current_efforts = {}23 22 24 23 … … 47 46 if not self.paused: 48 47 # Make a new effort to complete the task. 49 e = FutileEffort(taskid=task.id) 50 e.flush() 51 e.start() 48 start_effort(task) 52 49 53 50 def run_next(self): … … 58 55 tasks = FutileTask.select( 59 56 and_(FutileTask.c.node==self.node, 60 FutileTask.c.nexteffort <= now), 57 FutileTask.c.nexteffort <= now, 58 ), 61 59 order_by=['nexteffort'], 62 60 limit=1) 63 61 if tasks: 62 print 'Running', tasks[0].id 64 63 self.run_task(tasks[0]) 65 64 return … … 68 67 tasks = FutileTask.select( 69 68 and_(FutileTask.c.node==self.node, 70 FutileTask.c.nexteffort >= now), 69 FutileTask.c.nexteffort >= now, 70 ), 71 71 order_by=['nexteffort'], 72 72 limit=1) … … 75 75 delay = (delay.days * 86400) + delay.seconds 76 76 if delay <= 0: 77 delay = 0 77 self.run_task(tasks[0]) 78 return 79 78 80 if delay > self.idle_poll_rate: 79 81 # Why not just: … … 94 96 delay = self.idle_poll_rate 95 97 98 print 'Sleeping', delay 96 99 time.sleep(delay) 97 100 … … 105 108 threading.Thread(target=self.run_continuously).start() 106 109 110 def start_effort(task): 111 """Start a futile effort to perform the given futile task in its own thread.""" 112 e = FutileEffort(taskid=task.id) 113 t = threading.Thread( 114 target=run_effort, args=(e.id, task.name, task.kwargs)) 115 t.setName("FutileEffort %d (%r)" % (e.id, task.name)) 116 t.start() 117 118 def run_effort(effortid, taskname, kwargs): 119 """Run the given futile effort to perform a futile task.""" 120 executor = None 121 try: 122 futileefforts = FutileEffort.mapper.local_table 123 futileefforts.update(futileefforts.c.id==effortid, 124 {'starttime': datetime.datetime.now()}).execute() 125 executor = task_executors[taskname] 126 executor(**kwargs) 127 except: 128 if hasattr(executor, 'on_error'): 129 executor.on_error() 130 else: 131 if hasattr(executor, 'on_success'): 132 executor.on_success() 133 finally: 134 futileefforts.update(futileefforts.c.id==effortid, 135 {'endtime': datetime.datetime.now()}).execute() 136 107 137 108 138 class FutileTask(object): 109 139 """A futile task which is attempted at regular intervals.""" 140 141 def __init__(self, **kwargs): 142 for k, v in kwargs.iteritems(): 143 setattr(self, k, v) 144 self.flush() 110 145 111 146 def _get_kwargs(self): … … 124 159 class FutileEffort(object): 125 160 126 def start(self): 127 """Start this futile effort to perform a futile task in its own thread.""" 128 t = threading.Thread(target=self.run) 129 t.setName("FutileEffort %d (%r)" % (self.id, self.task.name)) 130 current_efforts[self.id] = self 131 t.start() 161 def __init__(self, **kwargs): 162 for k, v in kwargs.iteritems(): 163 setattr(self, k, v) 164 self.flush() 132 165 133 def run(self): 134 """Run this futile effort to perform a futile task.""" 135 try: 136 self.starttime = datetime.datetime.now() 137 self.flush() 138 self.executor = task_executors[self.task.name] 139 self.executor(**self.task.kwargs) 140 except: 141 self.error = traceback.format_exc() 142 if hasattr(self.executor, 'on_error'): 143 self.executor.on_error() 144 else: 145 if hasattr(self.executor, 'on_success'): 146 self.executor.on_success() 147 finally: 148 self.endtime = datetime.datetime.now() 149 self.flush() 150 166 def pending_effort(cls, taskid): 167 """Return the most recent unfinished effort, or None.""" 168 efforts = cls.select( 169 and_(cls.c.taskid==taskid, cls.c.starttime!=None, cls.c.endtime==None), 170 order_by=[desc('starttime')], 171 limit=1) 172 if efforts: 173 return efforts[0] 174 return None 175 pending_effort = classmethod(pending_effort) 151 176 152 177 def connect(metadata): … … 158 183 Column('nexteffort', DateTime), 159 184 Column('interval', Integer), 160 Column('enabled', Boolean),161 185 ) 162 186 futility/root.py
r183 r184 8 8 9 9 import datetime 10 delta_zero = datetime.timedelta(0) 10 11 import os 11 12 thisdir = os.path.abspath(os.path.dirname(__file__)) … … 14 15 import cherrypy 15 16 import futility 16 from futility import FutileTask, FutileEffort17 18 17 futility._incr.on_error = lambda: cherrypy.log('Error in increment test', traceback=True) 18 19 from futility import FutileTask, FutileEffort, start_effort 20 21 from sqlalchemy import objectstore 22 19 23 20 24 def formatdt(dt): … … 22 26 return '' 23 27 return dt.strftime('%Y-%m-%d %H:%M:%S') 28 29 def formattd(td): 30 if td is None: 31 return '' 32 if td.days: 33 return '%d d' % td.days 34 elif td.seconds > 60: 35 return '%d m %d s' % divmod(td.seconds, 60) 36 elif td.seconds: 37 return '%d s' % td.seconds 38 return '%d ms' % int(td.microseconds / 1000) 39 40 41 def cleanup_sqla(debug=False): 42 if debug: 43 cherrypy.log('Running cleanup.', 'TOOLS.SQLA') 44 try: 45 objectstore.flush() 46 finally: 47 objectstore.clear() 48 cherrypy.tools.sqla = cherrypy.Tool('before_finalize', cleanup_sqla) 24 49 25 50 … … 38 63 'node': task.node, 39 64 'interval': str(task.interval), 40 'enabled': ' checked="checked"' if task.enabled else '',41 65 } 42 66 43 67 @cherrypy.expose 44 68 def default(self, task, name=None, kwargs=None, node=None, 45 interval=None , enabled=None):69 interval=None): 46 70 if cherrypy.request.method == 'POST' and name is not None: 47 71 task.name = (None if name == 'None' else name) … … 49 73 task.node = int(node) 50 74 task.interval = int(interval) 51 if enabled == 'True':52 task.enabled = True53 now = datetime.datetime.now()54 task.nexteffort = now + datetime.timedelta(seconds=int(interval))55 else:56 task.enabled = False57 task.nexteffort = None58 task.flush()59 75 raise cherrypy.HTTPRedirect('/tasks/') 60 return open(self.default_template, 'rb').read() % self.default_info(task) 76 cherrypy.response.headers['Cache-Control'] = 'no-cache' 77 info = self.default_info(task) 78 return open(self.default_template, 'rb').read() % info 61 79 62 80 @cherrypy.expose 63 81 def run(self, task): 64 # Advance the task's 'nexteffort' attribute 65 if task. enabled:82 # Advance the task's 'nexteffort' attribute. 83 if task.nexteffort is not None: 66 84 task.nexteffort = (datetime.datetime.now() + 67 85 datetime.timedelta(seconds=task.interval)) 68 task.flush()69 86 70 87 # Make a new effort to complete the task. 71 e = FutileEffort(taskid=task.id) 72 e.flush() 73 e.start() 88 start_effort(task) 89 raise cherrypy.HTTPRedirect('/tasks/') 90 91 @cherrypy.expose 92 def start(self, task): 93 # Advance the task's 'nexteffort' attribute. 94 task.nexteffort = (datetime.datetime.now() + 95 datetime.timedelta(seconds=task.interval)) 96 raise cherrypy.HTTPRedirect('/tasks/') 97 98 @cherrypy.expose 99 def stop(self, task): 100 task.nexteffort = None 74 101 raise cherrypy.HTTPRedirect('/tasks/') 75 102 taskresource = TaskResource() … … 84 111 for task in FutileTask.select(order_by=['name']): 85 112 interval = datetime.timedelta(seconds=task.interval) 113 114 effort = FutileEffort.pending_effort(task.id) 115 if effort: 116 cureffort = ('%s (%s)' % (formatdt(effort.starttime), 117 formattd(datetime.datetime.now() - effort.starttime))) 118 else: 119 cureffort = ( 120 '<form action="/tasks/%s/run" method="POST" style="display: inline">' 121 '<input type="submit" value="Run Once" /></form>' % task.id) 122 123 if task.nexteffort: 124 nexteffort = formatdt(task.nexteffort) 125 td = task.nexteffort - datetime.datetime.now() 126 if td >= delta_zero: 127 nexteffort += ' (%s)' % formattd(td) 128 else: 129 nexteffort += ' (now)' 130 nexteffort += ( 131 '<form action="/tasks/%s/stop" method="POST" style="display: inline">' 132 '<input type="submit" value="Stop" /></form>' % task.id) 133 else: 134 nexteffort = ( 135 '<form action="/tasks/%s/start" method="POST" style="display: inline">' 136 '<input type="submit" value="Start" /></form>' % task.id) 137 86 138 tasks.append(' <tr><td><a href="%s/">%s</a></td><td>%s</td>' 87 139 '<td>%s</td><td>%s</td><td>%s</td><td>%s</td>' 88 140 '<td>%s</td></tr>' % 89 141 (task.id, task.id, task.name, task.jsonkwargs, 90 str(task.node), str(interval), formatdt(task.nexteffort), 91 str(task.enabled))) 142 str(task.node), str(interval), cureffort, nexteffort)) 92 143 return {'tasks': '\n'.join(tasks), 93 144 'increment_test': futility.increment_test[0], … … 96 147 @cherrypy.expose 97 148 def index(self): 149 cherrypy.response.headers['Cache-Control'] = 'no-cache' 98 150 return open(self.index_template, 'rb').read() % self.index_info() 99 151 100 152 @cherrypy.expose 101 153 def new(self): 102 task = FutileTask(name=None, kwargs='{}', interval=3600, enabled=False) 103 task.flush() 154 task = FutileTask(name=None, kwargs={}, interval=3600) 104 155 raise cherrypy.HTTPRedirect('/tasks/%d' % task.id) 105 156 … … 141 192 142 193 app_config = { 194 '/': { 195 'tools.sqla.on': True, 196 }, 143 197 '/futility.css': { 144 198 'tools.staticfile.on': True, futility/task.html
r183 r184 20 20 <tr><th>Interval</th> 21 21 <td><input type="text" name="interval" value="%(interval)s" size="20" /> seconds</td></tr> 22 <tr><th>Enabled</th>23 <td><input type="checkbox" name="enabled" value="True" %(enabled)s /></td></tr>24 22 </table> 25 23 <input type="submit" value="Submit" /> 26 24 </form> 27 25 <form action="/tasks/%(taskid)s/run" method="POST"> 28 <input type="submit" value="Run Now" />26 <input type="submit" value="Run Once" /> 29 27 </form> 30 28 </body> futility/tasks.html
r183 r184 45 45 <table class='tasks'> 46 46 <tr><th>ID</th><th>Name</th><th>Args</th><th>Node</th> 47 <th>Interval</th><th>Next Effort</th><th>Enabled</th></tr> 47 <th>Interval</th><th>Current Effort</th> 48 <th>Next Effort</th></tr> 48 49 %(tasks)s 49 50 </table>
