You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
194 lines
5.2 KiB
194 lines
5.2 KiB
# Copyright 2010 Google Inc. All Rights Reserved. |
|
# |
|
|
|
import logging |
|
import os |
|
import re |
|
import threading |
|
|
|
from automation.common import job |
|
from automation.common import logger |
|
from automation.server.job_executer import JobExecuter |
|
|
|
|
|
class IdProducerPolicy(object): |
|
"""Produces series of unique integer IDs. |
|
|
|
Example: |
|
id_producer = IdProducerPolicy() |
|
id_a = id_producer.GetNextId() |
|
id_b = id_producer.GetNextId() |
|
assert id_a != id_b |
|
""" |
|
|
|
def __init__(self): |
|
self._counter = 1 |
|
|
|
def Initialize(self, home_prefix, home_pattern): |
|
"""Find first available ID based on a directory listing. |
|
|
|
Args: |
|
home_prefix: A directory to be traversed. |
|
home_pattern: A regexp describing all files/directories that will be |
|
considered. The regexp must contain exactly one match group with name |
|
"id", which must match an integer number. |
|
|
|
Example: |
|
id_producer.Initialize(JOBDIR_PREFIX, 'job-(?P<id>\d+)') |
|
""" |
|
harvested_ids = [] |
|
|
|
if os.path.isdir(home_prefix): |
|
for filename in os.listdir(home_prefix): |
|
path = os.path.join(home_prefix, filename) |
|
|
|
if os.path.isdir(path): |
|
match = re.match(home_pattern, filename) |
|
|
|
if match: |
|
harvested_ids.append(int(match.group('id'))) |
|
|
|
self._counter = max(harvested_ids or [0]) + 1 |
|
|
|
def GetNextId(self): |
|
"""Calculates another ID considered to be unique.""" |
|
new_id = self._counter |
|
self._counter += 1 |
|
return new_id |
|
|
|
|
|
class JobManager(threading.Thread): |
|
|
|
def __init__(self, machine_manager): |
|
threading.Thread.__init__(self, name=self.__class__.__name__) |
|
self.all_jobs = [] |
|
self.ready_jobs = [] |
|
self.job_executer_mapping = {} |
|
|
|
self.machine_manager = machine_manager |
|
|
|
self._lock = threading.Lock() |
|
self._jobs_available = threading.Condition(self._lock) |
|
self._exit_request = False |
|
|
|
self.listeners = [] |
|
self.listeners.append(self) |
|
|
|
self._id_producer = IdProducerPolicy() |
|
self._id_producer.Initialize(job.Job.WORKDIR_PREFIX, 'job-(?P<id>\d+)') |
|
|
|
self._logger = logging.getLogger(self.__class__.__name__) |
|
|
|
def StartJobManager(self): |
|
self._logger.info('Starting...') |
|
|
|
with self._lock: |
|
self.start() |
|
self._jobs_available.notifyAll() |
|
|
|
def StopJobManager(self): |
|
self._logger.info('Shutdown request received.') |
|
|
|
with self._lock: |
|
for job_ in self.all_jobs: |
|
self._KillJob(job_.id) |
|
|
|
# Signal to die |
|
self._exit_request = True |
|
self._jobs_available.notifyAll() |
|
|
|
# Wait for all job threads to finish |
|
for executer in self.job_executer_mapping.values(): |
|
executer.join() |
|
|
|
def KillJob(self, job_id): |
|
"""Kill a job by id. |
|
|
|
Does not block until the job is completed. |
|
""" |
|
with self._lock: |
|
self._KillJob(job_id) |
|
|
|
def GetJob(self, job_id): |
|
for job_ in self.all_jobs: |
|
if job_.id == job_id: |
|
return job_ |
|
return None |
|
|
|
def _KillJob(self, job_id): |
|
self._logger.info('Killing [Job: %d].', job_id) |
|
|
|
if job_id in self.job_executer_mapping: |
|
self.job_executer_mapping[job_id].Kill() |
|
for job_ in self.ready_jobs: |
|
if job_.id == job_id: |
|
self.ready_jobs.remove(job_) |
|
break |
|
|
|
def AddJob(self, job_): |
|
with self._lock: |
|
job_.id = self._id_producer.GetNextId() |
|
|
|
self.all_jobs.append(job_) |
|
# Only queue a job as ready if it has no dependencies |
|
if job_.is_ready: |
|
self.ready_jobs.append(job_) |
|
|
|
self._jobs_available.notifyAll() |
|
|
|
return job_.id |
|
|
|
def CleanUpJob(self, job_): |
|
with self._lock: |
|
if job_.id in self.job_executer_mapping: |
|
self.job_executer_mapping[job_.id].CleanUpWorkDir() |
|
del self.job_executer_mapping[job_.id] |
|
# TODO(raymes): remove job from self.all_jobs |
|
|
|
def NotifyJobComplete(self, job_): |
|
self.machine_manager.ReturnMachines(job_.machines) |
|
|
|
with self._lock: |
|
self._logger.debug('Handling %r completion event.', job_) |
|
|
|
if job_.status == job.STATUS_SUCCEEDED: |
|
for succ in job_.successors: |
|
if succ.is_ready: |
|
if succ not in self.ready_jobs: |
|
self.ready_jobs.append(succ) |
|
|
|
self._jobs_available.notifyAll() |
|
|
|
def AddListener(self, listener): |
|
self.listeners.append(listener) |
|
|
|
@logger.HandleUncaughtExceptions |
|
def run(self): |
|
self._logger.info('Started.') |
|
|
|
while not self._exit_request: |
|
with self._lock: |
|
# Get the next ready job, block if there are none |
|
self._jobs_available.wait() |
|
|
|
while self.ready_jobs: |
|
ready_job = self.ready_jobs.pop() |
|
|
|
required_machines = ready_job.machine_dependencies |
|
for pred in ready_job.predecessors: |
|
required_machines[0].AddPreferredMachine( |
|
pred.primary_machine.hostname) |
|
|
|
machines = self.machine_manager.GetMachines(required_machines) |
|
if not machines: |
|
# If we can't get the necessary machines right now, simply wait |
|
# for some jobs to complete |
|
self.ready_jobs.insert(0, ready_job) |
|
break |
|
else: |
|
# Mark as executing |
|
executer = JobExecuter(ready_job, machines, self.listeners) |
|
executer.start() |
|
self.job_executer_mapping[ready_job.id] = executer |
|
|
|
self._logger.info('Stopped.')
|
|
|