X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/25f9901f29c9e5fe372aa54023b4097ec96b2233..6ee7102ab0546ac15748905a9ac73e04123304ab:/lib/jqueue.py diff --git a/lib/jqueue.py b/lib/jqueue.py index 9aa0bab..5dd5578 100644 --- a/lib/jqueue.py +++ b/lib/jqueue.py @@ -145,20 +145,18 @@ class _QueuedJob(object): @ivar id: the job ID @type ops: list @ivar ops: the list of _QueuedOpCode that constitute the job - @type run_op_index: int - @ivar run_op_index: the currently executing opcode, or -1 if - we didn't yet start executing @type log_serial: int @ivar log_serial: holds the index for the next log entry @ivar received_timestamp: the timestamp for when the job was received @ivar start_timestmap: the timestamp for start of execution @ivar end_timestamp: the timestamp for end of execution + @ivar lock_status: In-memory locking information for debugging @ivar change: a Condition variable we use for waiting for job changes """ - __slots__ = ["queue", "id", "ops", "run_op_index", "log_serial", + __slots__ = ["queue", "id", "ops", "log_serial", "received_timestamp", "start_timestamp", "end_timestamp", - "change", + "lock_status", "change", "__weakref__"] def __init__(self, queue, job_id, ops): @@ -180,12 +178,14 @@ class _QueuedJob(object): self.queue = queue self.id = job_id self.ops = [_QueuedOpCode(op) for op in ops] - self.run_op_index = -1 self.log_serial = 0 self.received_timestamp = TimeStampNow() self.start_timestamp = None self.end_timestamp = None + # In-memory attributes + self.lock_status = None + # Condition to wait for changes self.change = threading.Condition(self.queue._lock) @@ -204,11 +204,13 @@ class _QueuedJob(object): obj = _QueuedJob.__new__(cls) obj.queue = queue obj.id = state["id"] - obj.run_op_index = state["run_op_index"] obj.received_timestamp = state.get("received_timestamp", None) obj.start_timestamp = state.get("start_timestamp", None) obj.end_timestamp = state.get("end_timestamp", None) + # In-memory attributes + obj.lock_status = None + obj.ops = [] obj.log_serial = 0 for op_state in state["ops"]: @@ -232,7 +234,6 @@ class _QueuedJob(object): return { "id": self.id, "ops": [op.Serialize() for op in self.ops], - "run_op_index": self.run_op_index, "start_timestamp": self.start_timestamp, "end_timestamp": self.end_timestamp, "received_timestamp": self.received_timestamp, @@ -334,35 +335,90 @@ class _QueuedJob(object): not_marked = False -class _JobQueueWorker(workerpool.BaseWorker): - """The actual job workers. +class _OpExecCallbacks(mcpu.OpExecCbBase): + def __init__(self, queue, job, op): + """Initializes this class. - """ - def _NotifyStart(self): + @type queue: L{JobQueue} + @param queue: Job queue + @type job: L{_QueuedJob} + @param job: Job object + @type op: L{_QueuedOpCode} + @param op: OpCode + + """ + assert queue, "Queue is missing" + assert job, "Job is missing" + assert op, "Opcode is missing" + + self._queue = queue + self._job = job + self._op = op + + def NotifyStart(self): """Mark the opcode as running, not lock-waiting. - This is called from the mcpu code as a notifier function, when the - LU is finally about to start the Exec() method. Of course, to have - end-user visible results, the opcode must be initially (before - calling into Processor.ExecOpCode) set to OP_STATUS_WAITLOCK. + This is called from the mcpu code as a notifier function, when the LU is + finally about to start the Exec() method. Of course, to have end-user + visible results, the opcode must be initially (before calling into + Processor.ExecOpCode) set to OP_STATUS_WAITLOCK. """ - assert self.queue, "Queue attribute is missing" - assert self.opcode, "Opcode attribute is missing" - - self.queue.acquire() + self._queue.acquire() try: - assert self.opcode.status in (constants.OP_STATUS_WAITLOCK, - constants.OP_STATUS_CANCELING) + assert self._op.status in (constants.OP_STATUS_WAITLOCK, + constants.OP_STATUS_CANCELING) + + # All locks are acquired by now + self._job.lock_status = None # Cancel here if we were asked to - if self.opcode.status == constants.OP_STATUS_CANCELING: + if self._op.status == constants.OP_STATUS_CANCELING: raise CancelJob() - self.opcode.status = constants.OP_STATUS_RUNNING + self._op.status = constants.OP_STATUS_RUNNING finally: - self.queue.release() + self._queue.release() + def Feedback(self, *args): + """Append a log entry. + + """ + assert len(args) < 3 + + if len(args) == 1: + log_type = constants.ELOG_MESSAGE + log_msg = args[0] + else: + (log_type, log_msg) = args + + # The time is split to make serialization easier and not lose + # precision. + timestamp = utils.SplitTime(time.time()) + + self._queue.acquire() + try: + self._job.log_serial += 1 + self._op.log.append((self._job.log_serial, timestamp, log_type, log_msg)) + + self._job.change.notifyAll() + finally: + self._queue.release() + + def ReportLocks(self, msg): + """Write locking information to the job. + + Called whenever the LU processor is waiting for a lock or has acquired one. + + """ + # Not getting the queue lock because this is a single assignment + self._job.lock_status = msg + + +class _JobQueueWorker(workerpool.BaseWorker): + """The actual job workers. + + """ def RunTask(self, job): """Job executor. @@ -376,7 +432,7 @@ class _JobQueueWorker(workerpool.BaseWorker): logging.info("Worker %s processing job %s", self.worker_id, job.id) proc = mcpu.Processor(self.pool.queue.context) - self.queue = queue = job.queue + queue = job.queue try: try: count = len(job.ops) @@ -400,7 +456,6 @@ class _JobQueueWorker(workerpool.BaseWorker): if op.status == constants.OP_STATUS_CANCELED: raise CancelJob() assert op.status == constants.OP_STATUS_QUEUED - job.run_op_index = idx op.status = constants.OP_STATUS_WAITLOCK op.result = None op.start_timestamp = TimeStampNow() @@ -412,34 +467,9 @@ class _JobQueueWorker(workerpool.BaseWorker): finally: queue.release() - def _Log(*args): - """Append a log entry. - - """ - assert len(args) < 3 - - if len(args) == 1: - log_type = constants.ELOG_MESSAGE - log_msg = args[0] - else: - log_type, log_msg = args - - # The time is split to make serialization easier and not lose - # precision. - timestamp = utils.SplitTime(time.time()) - - queue.acquire() - try: - job.log_serial += 1 - op.log.append((job.log_serial, timestamp, log_type, log_msg)) - - job.change.notifyAll() - finally: - queue.release() - - # Make sure not to hold lock while _Log is called - self.opcode = op - result = proc.ExecOpCode(input_opcode, _Log, self._NotifyStart) + # Make sure not to hold queue lock while calling ExecOpCode + result = proc.ExecOpCode(input_opcode, + _OpExecCallbacks(queue, job, op)) queue.acquire() try: @@ -460,7 +490,10 @@ class _JobQueueWorker(workerpool.BaseWorker): try: try: op.status = constants.OP_STATUS_ERROR - op.result = str(err) + if isinstance(err, errors.GenericError): + op.result = errors.EncodeException(err) + else: + op.result = str(err) op.end_timestamp = TimeStampNow() logging.info("Op %s/%s: Error in opcode %s: %s", idx + 1, count, op_summary, err) @@ -484,7 +517,7 @@ class _JobQueueWorker(workerpool.BaseWorker): queue.acquire() try: try: - job.run_op_index = -1 + job.lock_status = None job.end_timestamp = TimeStampNow() queue.UpdateJobUnlocked(job) finally: @@ -492,6 +525,7 @@ class _JobQueueWorker(workerpool.BaseWorker): status = job.CalcStatus() finally: queue.release() + logging.info("Worker %s finished job %s, status = %s", self.worker_id, job_id, status) @@ -639,7 +673,7 @@ class JobQueue(object): # Clean queue directory on added node result = rpc.RpcRunner.call_jobqueue_purge(node_name) - msg = result.RemoteFailMsg() + msg = result.fail_msg if msg: logging.warning("Cannot cleanup queue directory on node %s: %s", node_name, msg) @@ -658,16 +692,12 @@ class JobQueue(object): for file_name in files: # Read file content - fd = open(file_name, "r") - try: - content = fd.read() - finally: - fd.close() + content = utils.ReadFile(file_name) result = rpc.RpcRunner.call_jobqueue_update([node_name], [node.primary_ip], file_name, content) - msg = result[node_name].RemoteFailMsg() + msg = result[node_name].fail_msg if msg: logging.error("Failed to upload file %s to node %s: %s", file_name, node_name, msg) @@ -707,7 +737,7 @@ class JobQueue(object): success = [] for node in nodes: - msg = result[node].RemoteFailMsg() + msg = result[node].fail_msg if msg: failed.append(node) logging.error("RPC call %s failed on node %s: %s", @@ -802,26 +832,31 @@ class JobQueue(object): """ return str(int(job_id) / JOBS_PER_ARCHIVE_DIRECTORY) - def _NewSerialUnlocked(self): + def _NewSerialsUnlocked(self, count): """Generates a new job identifier. Job identifiers are unique during the lifetime of a cluster. + @type count: integer + @param count: how many serials to return @rtype: str @return: a string representing the job identifier. """ + assert count > 0 # New number - serial = self._last_serial + 1 + serial = self._last_serial + count # Write to file self._WriteAndReplicateFileUnlocked(constants.JOB_QUEUE_SERIAL_FILE, "%s\n" % serial) + result = [self._FormatJobID(v) + for v in range(self._last_serial, serial + 1)] # Keep it only if we were able to write the file self._last_serial = serial - return self._FormatJobID(serial) + return result @staticmethod def _GetJobPath(job_id): @@ -914,15 +949,13 @@ class JobQueue(object): filepath = self._GetJobPath(job_id) logging.debug("Loading job from %s", filepath) try: - fd = open(filepath, "r") + raw_data = utils.ReadFile(filepath) except IOError, err: if err.errno in (errno.ENOENT, ): return None raise - try: - data = serializer.LoadJson(fd.read()) - finally: - fd.close() + + data = serializer.LoadJson(raw_data) try: job = _QueuedJob.Restore(self, data) @@ -987,12 +1020,14 @@ class JobQueue(object): return True @_RequireOpenQueue - def _SubmitJobUnlocked(self, ops): + def _SubmitJobUnlocked(self, job_id, ops): """Create and store a new job. This enters the job into our job queue and also puts it on the new queue, in order for it to be picked up by the queue processors. + @type job_id: job ID + @param jod_id: the job ID for the new job @type ops: list @param ops: The list of OpCodes that will become the new job. @rtype: job ID @@ -1014,8 +1049,6 @@ class JobQueue(object): if size >= constants.JOB_QUEUE_SIZE_HARD_LIMIT: raise errors.JobQueueFull() - # Get job identifier - job_id = self._NewSerialUnlocked() job = _QueuedJob(self, job_id, ops) # Write to disk @@ -1037,7 +1070,8 @@ class JobQueue(object): @see: L{_SubmitJobUnlocked} """ - return self._SubmitJobUnlocked(ops) + job_id = self._NewSerialsUnlocked(1)[0] + return self._SubmitJobUnlocked(job_id, ops) @utils.LockedMethod @_RequireOpenQueue @@ -1048,9 +1082,10 @@ class JobQueue(object): """ results = [] - for ops in jobs: + all_job_ids = self._NewSerialsUnlocked(len(jobs)) + for job_id, ops in zip(all_job_ids, jobs): try: - data = self._SubmitJobUnlocked(ops) + data = self._SubmitJobUnlocked(job_id, ops) status = True except errors.GenericError, err: data = str(err) @@ -1059,7 +1094,6 @@ class JobQueue(object): return results - @_RequireOpenQueue def UpdateJobUnlocked(self, job): """Update a job's on disk storage. @@ -1107,6 +1141,10 @@ class JobQueue(object): """ logging.debug("Waiting for changes in job %s", job_id) + + job_info = None + log_entries = None + end_time = time.time() + timeout while True: delta_time = end_time - time.time() @@ -1148,7 +1186,10 @@ class JobQueue(object): logging.debug("Job %s changed", job_id) - return (job_info, log_entries) + if job_info is None and log_entries is None: + return None + else: + return (job_info, log_entries) @utils.LockedMethod @_RequireOpenQueue @@ -1172,8 +1213,8 @@ class JobQueue(object): if job_status not in (constants.JOB_STATUS_QUEUED, constants.JOB_STATUS_WAITLOCK): - logging.debug("Job %s is no longer in the queue", job.id) - return (False, "Job %s is no longer in the queue" % job.id) + logging.debug("Job %s is no longer waiting in the queue", job.id) + return (False, "Job %s is no longer waiting in the queue" % job.id) if job_status == constants.JOB_STATUS_QUEUED: self.CancelJobUnlocked(job) @@ -1346,6 +1387,8 @@ class JobQueue(object): row.append(job.start_timestamp) elif fname == "end_ts": row.append(job.end_timestamp) + elif fname == "lock_status": + row.append(job.lock_status) elif fname == "summary": row.append([op.input.Summary() for op in job.ops]) else: