From 76b620280a38d782da0c277974cce6cd459546bf Mon Sep 17 00:00:00 2001 From: Iustin Pop <iustin@google.com> Date: Thu, 2 Aug 2012 13:47:53 +0200 Subject: [PATCH] Switch job IDs to numeric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This has been a long-standing cleanup item, which we've always refrained from doing due to the high estimated effort needed. In reality, it turned out that after some infrastructure improvements (the previous patches), the actual job queue-related changes are quite small. We will need to update the NEWS file later, but so far the RAPI documentation doesn't mention that the job ID is a string (it only says it is "a number"), so it doesn't look like it needs update. Signed-off-by: Iustin Pop <iustin@google.com> Reviewed-by: RenΓ© Nussbaumer <rn@google.com> --- htools/Ganeti/HTools/Program/Hbal.hs | 2 +- htools/Ganeti/Luxi.hs | 28 ++++++++++------ lib/client/gnt_debug.py | 3 +- lib/client/gnt_job.py | 23 ++++++++++--- lib/jqueue.py | 50 ++++++++++++++-------------- lib/jstore.py | 13 ++++---- lib/qlang.py | 2 +- lib/query.py | 3 +- qa/qa_rapi.py | 3 +- test/ganeti.jqueue_unittest.py | 6 ++-- test/ganeti.jstore_unittest.py | 6 ++-- test/ganeti.query_unittest.py | 23 ++++++++----- 12 files changed, 96 insertions(+), 66 deletions(-) diff --git a/htools/Ganeti/HTools/Program/Hbal.hs b/htools/Ganeti/HTools/Program/Hbal.hs index bbc62c32e..19880a768 100644 --- a/htools/Ganeti/HTools/Program/Hbal.hs +++ b/htools/Ganeti/HTools/Program/Hbal.hs @@ -205,7 +205,7 @@ execJobSet master nl il cref (js:jss) = do case jids of Bad x -> return $ Bad x Ok x -> do - putStrLn $ "Got job IDs " ++ commaJoin x + putStrLn $ "Got job IDs " ++ commaJoin (map show x) waitForJobs client x ) case jrs of diff --git a/htools/Ganeti/Luxi.hs b/htools/Ganeti/Luxi.hs index 7206c9b9f..d8c243c23 100644 --- a/htools/Ganeti/Luxi.hs +++ b/htools/Ganeti/Luxi.hs @@ -44,6 +44,7 @@ module Ganeti.Luxi ) where import Data.IORef +import Data.Ratio (numerator, denominator) import Control.Monad import Text.JSON (encodeStrict, decodeStrict) import qualified Text.JSON as J @@ -73,7 +74,7 @@ withTimeout secs descr action = do -- * Generic protocol functionality -- | The Ganeti job type. -type JobId = String +type JobId = Int $(declareSADT "QrViaLuxi" [ ("QRLock", 'qrLock) @@ -107,7 +108,7 @@ $(genLuxiOp "LuxiOp" , ("lock", [t| Bool |], [| id |]) ]) , (luxiReqQueryJobs, - [ ("ids", [t| [Int] |], [| map show |]) + [ ("ids", [t| [Int] |], [| id |]) , ("fields", [t| [String] |], [| id |]) ]) , (luxiReqQueryExports, @@ -129,21 +130,21 @@ $(genLuxiOp "LuxiOp" [ ("ops", [t| [[OpCode]] |], [| id |]) ] ) , (luxiReqWaitForJobChange, - [ ("job", [t| Int |], [| show |]) + [ ("job", [t| Int |], [| id |]) , ("fields", [t| [String]|], [| id |]) , ("prev_job", [t| JSValue |], [| id |]) , ("prev_log", [t| JSValue |], [| id |]) , ("tmout", [t| Int |], [| id |]) ]) , (luxiReqArchiveJob, - [ ("job", [t| Int |], [| show |]) ] + [ ("job", [t| Int |], [| id |]) ] ) , (luxiReqAutoArchiveJobs, [ ("age", [t| Int |], [| id |]) , ("tmout", [t| Int |], [| id |]) ]) , (luxiReqCancelJob, - [ ("job", [t| Int |], [| show |]) ] + [ ("job", [t| Int |], [| id |]) ] ) , (luxiReqSetDrainFlag, [ ("flag", [t| Bool |], [| id |]) ] @@ -267,7 +268,7 @@ decodeCall (LuxiCall call args) = case call of ReqQueryJobs -> do (jid, jargs) <- fromJVal args - rid <- mapM (tryRead "parsing job ID" . fromJSString) jid + rid <- mapM parseJobId jid let rargs = map fromJSString jargs return $ QueryJobs rid rargs ReqQueryInstances -> do @@ -307,11 +308,11 @@ decodeCall (LuxiCall call args) = J.readJSON d `ap` J.readJSON e _ -> J.Error "Not enough values" - rid <- tryRead "parsing job ID" jid + rid <- parseJobId jid return $ WaitForJobChange rid fields pinfo pidx wtmout ReqArchiveJob -> do [jid] <- fromJVal args - rid <- tryRead "parsing job ID" jid + rid <- parseJobId jid return $ ArchiveJob rid ReqAutoArchiveJobs -> do (age, tmout) <- fromJVal args @@ -327,7 +328,7 @@ decodeCall (LuxiCall call args) = return $ QueryTags kind name ReqCancelJob -> do [job] <- fromJVal args - rid <- tryRead "parsing job ID" job + rid <- parseJobId job return $ CancelJob rid ReqSetDrainFlag -> do [flag] <- fromJVal args @@ -359,7 +360,12 @@ callMethod method s = do -- | Parses a job ID. parseJobId :: JSValue -> Result JobId -parseJobId (JSString x) = Ok $ fromJSString x +parseJobId (JSString x) = tryRead "parsing job id" . fromJSString $ x +parseJobId (JSRational _ x) = + if denominator x /= 1 + then Bad $ "Got fractional job ID from master daemon?! Value:" ++ show x + -- FIXME: potential integer overflow here on 32-bit platforms + else Ok . fromIntegral . numerator $ x parseJobId x = Bad $ "Wrong type/value for job id: " ++ show x -- | Parse job submission result. @@ -383,7 +389,7 @@ submitManyJobs s jobs = do -- | Custom queryJobs call. queryJobsStatus :: Client -> [JobId] -> IO (Result [JobStatus]) queryJobsStatus s jids = do - rval <- callMethod (QueryJobs (map read jids) ["status"]) s + rval <- callMethod (QueryJobs jids ["status"]) s return $ case rval of Bad x -> Bad x Ok y -> case J.readJSON y::(J.Result [[JobStatus]]) of diff --git a/lib/client/gnt_debug.py b/lib/client/gnt_debug.py index ee458dabe..f22d64ae6 100644 --- a/lib/client/gnt_debug.py +++ b/lib/client/gnt_debug.py @@ -312,7 +312,8 @@ def _TestJobSubmission(opts): result = cl.SubmitManyJobs(jobs) if not (len(result) == 2 and compat.all(len(i) == 2 for i in result) and - compat.all(isinstance(i[1], basestring) for i in result) and + isinstance(result[0][1], int) and + isinstance(result[1][1], basestring) and result[0][0] and not result[1][0]): raise errors.OpExecError("Submitting multiple jobs did not work as" " expected, result %s" % result) diff --git a/lib/client/gnt_job.py b/lib/client/gnt_job.py index 7d12ab369..960cc4900 100644 --- a/lib/client/gnt_job.py +++ b/lib/client/gnt_job.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2006, 2007 Google Inc. +# Copyright (C) 2006, 2007, 2012 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -60,6 +60,21 @@ def _FormatStatus(value): raise errors.ProgrammerError("Unknown job status code '%s'" % value) +def _ParseJobIds(args): + """Parses a list of string job IDs into integers. + + @param args: list of strings + @return: list of integers + @raise OpPrereqError: in case of invalid values + + """ + try: + return [int(a) for a in args] + except (ValueError, TypeError), err: + raise errors.OpPrereqError("Invalid job ID passed: %s" % err, + errors.ECODE_INVAL) + + def ListJobs(opts, args): """List the jobs @@ -85,7 +100,7 @@ def ListJobs(opts, args): opts.separator, not opts.no_headers, format_override=fmtoverride, verbose=opts.verbose, force_filter=opts.force_filter, namefield="id", - qfilter=qfilter) + qfilter=qfilter, isnumeric=True) def ListJobFields(opts, args): @@ -203,8 +218,8 @@ def ShowJobs(opts, args): "opstart", "opexec", "opend", "received_ts", "start_ts", "end_ts", ] - result = GetClient().Query(constants.QR_JOB, selected_fields, - qlang.MakeSimpleFilter("id", args)).data + qfilter = qlang.MakeSimpleFilter("id", _ParseJobIds(args)) + result = GetClient().Query(constants.QR_JOB, selected_fields, qfilter).data first = True diff --git a/lib/jqueue.py b/lib/jqueue.py index 3b34cbe83..af0e3595e 100644 --- a/lib/jqueue.py +++ b/lib/jqueue.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc. +# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -220,7 +220,7 @@ class _QueuedJob(object): raise errors.GenericError("A job needs at least one opcode") self.queue = queue - self.id = job_id + self.id = int(job_id) self.ops = [_QueuedOpCode(op) for op in ops] self.log_serial = 0 self.received_timestamp = TimeStampNow() @@ -267,7 +267,7 @@ class _QueuedJob(object): """ obj = _QueuedJob.__new__(cls) obj.queue = queue - obj.id = state["id"] + obj.id = int(state["id"]) obj.received_timestamp = state.get("received_timestamp", None) obj.start_timestamp = state.get("start_timestamp", None) obj.end_timestamp = state.get("end_timestamp", None) @@ -1384,14 +1384,14 @@ class _JobDependencyManager: @type job: L{_QueuedJob} @param job: Job object - @type dep_job_id: string + @type dep_job_id: int @param dep_job_id: ID of dependency job @type dep_status: list @param dep_status: Required status """ - assert ht.TString(job.id) - assert ht.TString(dep_job_id) + assert ht.TJobId(job.id) + assert ht.TJobId(dep_job_id) assert ht.TListOf(ht.TElemOf(constants.JOBS_FINALIZED))(dep_status) if job.id == dep_job_id: @@ -1446,11 +1446,11 @@ class _JobDependencyManager: @attention: Do not call until L{CheckAndRegister} returned a status other than C{WAITDEP} for C{job_id}, or behaviour is undefined - @type job_id: string + @type job_id: int @param job_id: Job ID """ - assert ht.TString(job_id) + assert ht.TJobId(job_id) self._lock.acquire() try: @@ -1803,8 +1803,8 @@ class JobQueue(object): @type count: integer @param count: how many serials to return - @rtype: str - @return: a string representing the job identifier. + @rtype: list of int + @return: a list of job identifiers. """ assert ht.TPositiveInt(count) @@ -1870,9 +1870,9 @@ class JobQueue(object): for filename in utils.ListVisibleFiles(constants.QUEUE_DIR): m = constants.JOB_FILE_RE.match(filename) if m: - jlist.append(m.group(1)) + jlist.append(int(m.group(1))) if sort: - jlist = utils.NiceSort(jlist) + jlist.sort() return jlist def _LoadJobUnlocked(self, job_id): @@ -1882,6 +1882,7 @@ class JobQueue(object): existing, or try to load the job from the disk. If loading from disk, it will also add the job to the cache. + @type job_id: int @param job_id: the job id @rtype: L{_QueuedJob} or None @return: either None or the job object @@ -1920,7 +1921,7 @@ class JobQueue(object): Given a job file, read, load and restore it in a _QueuedJob format. - @type job_id: string + @type job_id: int @param job_id: job identifier @type try_archived: bool @param try_archived: Whether to try loading an archived job @@ -1968,7 +1969,7 @@ class JobQueue(object): In case of error reading the job, it gets returned as None, and the exception is logged. - @type job_id: string + @type job_id: int @param job_id: job identifier @type try_archived: bool @param try_archived: Whether to try loading an archived job @@ -2181,14 +2182,11 @@ class JobQueue(object): def _GetJobStatusForDependencies(self, job_id): """Gets the status of a job for dependencies. - @type job_id: string + @type job_id: int @param job_id: Job ID @raise errors.JobLost: If job can't be found """ - if not isinstance(job_id, basestring): - job_id = jstore.FormatJobID(job_id) - # Not using in-memory cache as doing so would require an exclusive lock # Try to load from disk @@ -2229,7 +2227,7 @@ class JobQueue(object): timeout): """Waits for changes in a job. - @type job_id: string + @type job_id: int @param job_id: Job identifier @type fields: list of strings @param fields: Which fields to check for changes @@ -2264,7 +2262,7 @@ class JobQueue(object): This will only succeed if the job has not started yet. - @type job_id: string + @type job_id: int @param job_id: job ID of job to be cancelled. """ @@ -2331,7 +2329,7 @@ class JobQueue(object): This is just a wrapper over L{_ArchiveJobsUnlocked}. - @type job_id: string + @type job_id: int @param job_id: Job ID of job to be archived. @rtype: bool @return: Whether job was archived @@ -2433,9 +2431,9 @@ class JobQueue(object): @param qfilter: Query filter """ - (qobj, ctx, sort_by_name) = self._Query(fields, qfilter) + (qobj, ctx, _) = self._Query(fields, qfilter) - return query.GetQueryResponse(qobj, ctx, sort_by_name=sort_by_name) + return query.GetQueryResponse(qobj, ctx, sort_by_name=False) def OldStyleQueryJobs(self, job_ids, fields): """Returns a list of jobs in queue. @@ -2449,11 +2447,13 @@ class JobQueue(object): the requested fields """ + # backwards compat: + job_ids = [int(jid) for jid in job_ids] qfilter = qlang.MakeSimpleFilter("id", job_ids) - (qobj, ctx, sort_by_name) = self._Query(fields, qfilter) + (qobj, ctx, _) = self._Query(fields, qfilter) - return qobj.OldStyleQuery(ctx, sort_by_name=sort_by_name) + return qobj.OldStyleQuery(ctx, sort_by_name=False) @locking.ssynchronized(_LOCK) def PrepareShutdown(self): diff --git a/lib/jstore.py b/lib/jstore.py index 1249e96a9..26be6caaf 100644 --- a/lib/jstore.py +++ b/lib/jstore.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2006, 2007 Google Inc. +# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -174,15 +174,14 @@ def SetDrainFlag(drain_flag): def FormatJobID(job_id): - """Convert a job ID to string format. + """Convert a job ID to int format. - Currently this just does C{str(job_id)} after performing some - checks, but if we want to change the job id format this will - abstract this change. + Currently this just is a no-op that performs some checks, but if we + want to change the job id format this will abstract this change. @type job_id: int or long @param job_id: the numeric job id - @rtype: str + @rtype: int @return: the formatted job id """ @@ -191,7 +190,7 @@ def FormatJobID(job_id): if job_id < 0: raise errors.ProgrammerError("Job ID %s is negative" % job_id) - return str(job_id) + return job_id def GetArchiveDirectory(job_id): diff --git a/lib/qlang.py b/lib/qlang.py index e52924337..af423e102 100644 --- a/lib/qlang.py +++ b/lib/qlang.py @@ -298,7 +298,7 @@ def _MakeFilterPart(namefield, text, isnumeric=False): try: number = int(text) except (TypeError, ValueError), err: - raise errors.OpPrereqError("Invalid integer passed: %s" % str(err), + raise errors.OpPrereqError("Invalid job ID passed: %s" % str(err), errors.ECODE_INVAL) return [OP_EQUAL, namefield, number] elif _CheckGlobbing(text): diff --git a/lib/query.py b/lib/query.py index db2030eb8..ae7332d89 100644 --- a/lib/query.py +++ b/lib/query.py @@ -746,6 +746,7 @@ class Query: (status, name) = _ProcessResult(self._name_fn(ctx, item)) assert status == constants.RS_NORMAL # TODO: Are there cases where we wouldn't want to use NiceSort? + # Answer: if the name field is non-string... result.append((utils.NiceSortKey(name), idx, row)) else: result.append(row) @@ -2267,7 +2268,7 @@ def _BuildJobFields(): """ fields = [ - (_MakeField("id", "ID", QFT_TEXT, "Job ID"), + (_MakeField("id", "ID", QFT_NUMBER, "Job ID"), None, QFF_JOB_ID, lambda _, (job_id, job): job_id), (_MakeField("status", "Status", QFT_TEXT, "Job status"), None, 0, _JobUnavail(lambda job: job.CalcStatus())), diff --git a/qa/qa_rapi.py b/qa/qa_rapi.py index 25778c75a..9fe3b7ce1 100644 --- a/qa/qa_rapi.py +++ b/qa/qa_rapi.py @@ -148,7 +148,8 @@ def _DoTests(uris): def _VerifyReturnsJob(data): - AssertMatch(data, r"^\d+$") + if not isinstance(data, int): + AssertMatch(data, r"^\d+$") def TestVersion(): diff --git a/test/ganeti.jqueue_unittest.py b/test/ganeti.jqueue_unittest.py index dcbad149c..65ded1a0c 100755 --- a/test/ganeti.jqueue_unittest.py +++ b/test/ganeti.jqueue_unittest.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -# Copyright (C) 2010, 2011 Google Inc. +# Copyright (C) 2010, 2011, 2012 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -702,7 +702,7 @@ class TestJobProcessor(unittest.TestCase, _JobProcessorTestUtils): # Check job status self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_ERROR) - self.assertEqual(job.GetInfo(["id"]), [str(job_id)]) + self.assertEqual(job.GetInfo(["id"]), [job_id]) self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_ERROR]) # Check opcode status @@ -926,7 +926,7 @@ class TestJobProcessor(unittest.TestCase, _JobProcessorTestUtils): for i in range(3)] # Create job - job_id = str(28492) + job_id = 28492 job = self._CreateJob(queue, job_id, ops) self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED) diff --git a/test/ganeti.jstore_unittest.py b/test/ganeti.jstore_unittest.py index abf17d18e..88512a6ef 100755 --- a/test/ganeti.jstore_unittest.py +++ b/test/ganeti.jstore_unittest.py @@ -36,10 +36,10 @@ import testutils class TestFormatJobID(testutils.GanetiTestCase): def test(self): - self.assertEqual(jstore.FormatJobID(0), "0") - self.assertEqual(jstore.FormatJobID(30498), "30498") + self.assertEqual(jstore.FormatJobID(0), 0) + self.assertEqual(jstore.FormatJobID(30498), 30498) self.assertEqual(jstore.FormatJobID(319472592764518609), - "319472592764518609") + 319472592764518609) def testErrors(self): for i in [-1, -2288, -9667, -0.205641, 0.0, 0.1, 13041.4472, "", "Hello", diff --git a/test/ganeti.query_unittest.py b/test/ganeti.query_unittest.py index 4789598c4..2bea10959 100755 --- a/test/ganeti.query_unittest.py +++ b/test/ganeti.query_unittest.py @@ -1162,14 +1162,19 @@ class TestQueryFilter(unittest.TestCase): for (what, fielddefs) in query.ALL_FIELDS.items(): if what == constants.QR_JOB: namefield = "id" - elif what == constants.QR_EXPORT: - namefield = "export" + nameval = 123 + namevalempty = 0 + genval = lambda i: i * 10 + randvals = [17361, 22015, 13193, 15215] else: - namefield = "name" - nameval = "abc" - namevalempty = "" - genval = lambda i: "x%s" % i - randvals = ["x17361", "x22015", "x13193", "x15215"] + nameval = "abc" + namevalempty = "" + genval = lambda i: "x%s" % i + randvals = ["x17361", "x22015", "x13193", "x15215"] + if what == constants.QR_EXPORT: + namefield = "export" + else: + namefield = "name" assert namefield in fielddefs @@ -1247,11 +1252,13 @@ class TestQueryFilter(unittest.TestCase): for (what, fielddefs) in query.ALL_FIELDS.items(): if what == constants.QR_JOB: namefield = "id" + nameval = 123 elif what == constants.QR_EXPORT: namefield = "export" + nameval = "value" else: namefield = "name" - nameval = "value" + nameval = "value" checks = [ [], ["="], ["=", "foo"], ["unknownop"], ["!"], -- GitLab