3 # Copyright 2009 the Melange authors.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
21 '"Sverre Rabbelier" <sverre@rabbelier.nl>',
22 '"Lennard de Rijk" <ljvderijk@gmail.com>',
28 from google
.appengine
.ext
import db
29 from google
.appengine
.runtime
import DeadlineExceededError
31 from soc
.cron
import student_proposal_mailer
32 from soc
.cron
import unique_user_id_adder
33 from soc
.models
.job
import Job
36 class Error(Exception):
37 """Base class for all exceptions raised by this module.
42 class FatalJobError(Error
):
43 """Class for all errors that lead to immediate job abortion.
48 class Handler(object):
49 """A handler that dispatches a cron job.
51 The tasks that are mapped into tasks will be called when a worker
52 has claimed the job. However, there is no guarantee as to how long
53 the task will be allowed to run. If an Exception is raised the task
54 is automatically rescheduled for execution.
58 """Constructs a new Handler with all known jobs set.
60 # pylint: disable-msg=C0103
62 self
.ALREADY_CLAIMED
= 1
68 self
.tasks
['setupStudentProposalMailing'] = \
69 student_proposal_mailer
.setupStudentProposalMailing
70 self
.tasks
['sendStudentProposalMail'] = \
71 student_proposal_mailer
.sendStudentProposalMail
72 self
.tasks
['setupUniqueUserIdAdder'] = \
73 unique_user_id_adder
.setupUniqueUserIdAdder
74 self
.tasks
['addUniqueUserIds'] = \
75 unique_user_id_adder
.addUniqueUserIds
77 def claimJob(self
, job_key
):
78 """A transaction to claim a job.
80 The transaction is rolled back if the status is not 'waiting'.
83 job
= Job
.get_by_id(job_key
)
85 if job
.status
!= 'waiting':
88 job
.status
= 'started'
90 # pylint: disable-msg=E1103
96 def timeoutJob(self
, job
):
99 If a job has timed out more than 50 times, the job is aborted.
104 if job
.timeouts
> 50:
105 job
.status
= 'aborted'
107 job
.status
= 'waiting'
111 job_id
= job
.key().id()
112 logging
.debug("job %d now timeout %d time(s)" % (job_id
, job
.timeouts
))
114 def failJob(self
, job
):
117 If the job has failed more than 5 times, the job is aborted.
123 job
.status
= 'aborted'
125 job
.status
= 'waiting'
129 job_id
= job
.key().id()
130 logging
.warning("job %d now failed %d time(s)" % (job_id
, job
.errors
))
132 def finishJob(self
, job
):
136 job
.status
= 'finished'
139 def abortJob(self
, job
):
143 job
.status
= 'aborted'
146 def handle(self
, job_key
):
149 Returns: one of the following status codes:
150 self.OUT_OF_TIME: returned when a DeadlineExceededError is raised
151 self.ALREADY_CLAIMED: if job.status is not 'waiting'
152 self.SUCCESS: if the job.status has been set to 'succes'
153 self.ABORTED: if the job.status has been set to 'aborted'
154 self.ERRORED: if the job encountered an error
160 job
= db
.run_in_transaction(self
.claimJob
, job_key
)
163 # someone already claimed the job
164 return self
.ALREADY_CLAIMED
166 if job
.task_name
not in self
.tasks
:
167 logging
.error("Unknown job %s" % job
.task_name
)
168 db
.run_in_transaction(self
.abortJob
, job_key
)
171 task
= self
.tasks
[job
.task_name
]
173 # execute the actual job
178 except DeadlineExceededError
, exception
:
181 return self
.OUT_OF_TIME
182 except FatalJobError
, exception
:
183 logging
.exception(exception
)
187 except Exception, exception
:
188 logging
.exception(exception
)
193 def iterate(self
, jobs
, retry_jobs
):
194 """Trivial iterator that iterates over jobs then retry_jobs