diff --git a/rq/job.py b/rq/job.py index afbc3a8..9563e1c 100644 --- a/rq/job.py +++ b/rq/job.py @@ -131,21 +131,53 @@ class Job(object): Will raise a NoSuchJobError if no corresponding Redis key exists. """ key = self.key - pickled_data = conn.hget(key, 'data') - if pickled_data is None: + properties = ['data', 'created_at', 'origin', 'description', + 'enqueued_at', 'ended_at', 'result', 'exc_info'] + data, created_at, origin, description, \ + enqueued_at, ended_at, result, \ + exc_info = conn.hmget(key, properties) + if data is None: raise NoSuchJobError('No such job: %s' % (key,)) - self.origin = conn.hget(key, 'origin') + def to_date(date_str): + if date_str is None: + return None + else: + return times.to_universal(date_str) + self._func, self._args, self._kwargs = unpickle(data) - self.created_at = times.to_universal(conn.hget(key, 'created_at')) + self.created_at = to_date(created_at) + self.origin = origin + self.description = description + self.enqueued_at = to_date(enqueued_at) + self.ended_at = to_date(ended_at) + self.result = result + self.exc_info = exc_info def save(self): """Persists the current job instance to its corresponding Redis key.""" key = self.key - conn.hset(key, 'data', pickled_data) - conn.hset(key, 'origin', self.origin) - conn.hset(key, 'created_at', times.format(self.created_at, 'UTC')) + + obj = {} + obj['created_at'] = times.format(self.created_at, 'UTC') + + if self.func is not None: + obj['data'] = dumps(self.job_tuple) + if self.origin is not None: + obj['origin'] = self.origin + if self.description is not None: + obj['description'] = self.description + if self.enqueued_at is not None: + obj['enqueued_at'] = times.format(self.enqueued_at, 'UTC') + if self.ended_at is not None: + obj['ended_at'] = times.format(self.ended_at, 'UTC') + if self.result is not None: + obj['result'] = self.result + if self.exc_info is not None: + obj['exc_info'] = self.exc_info + + conn.hmset(key, obj) # Job execution diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..32d47ed --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,6 @@ +import times + + +def strip_milliseconds(date): + return times.to_universal(times.format(date, 'UTC')) + diff --git a/tests/test_job.py b/tests/test_job.py index 112a5f2..c62c1d9 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -1,5 +1,7 @@ +import times from datetime import datetime from tests import RQTestCase +from tests.helpers import strip_milliseconds from pickle import loads from rq.job import Job from rq.exceptions import NoSuchJobError, UnpickleError @@ -14,28 +16,28 @@ class TestJob(RQTestCase): """Creation of new empty jobs.""" job = Job() - # Jobs have a random UUID + # Jobs have a random UUID and a creation date self.assertIsNotNone(job.id) - - # Jobs have no data yet... - self.assertEquals(job.func, None) - self.assertEquals(job.args, None) - self.assertEquals(job.kwargs, None) - self.assertEquals(job.origin, None) - self.assertEquals(job.enqueued_at, None) - self.assertEquals(job.result, None) - self.assertEquals(job.exc_info, None) - - # ...except for a created_at property self.assertIsNotNone(job.created_at) - def test_create_normal_job(self): + # ...and nothing else + self.assertIsNone(job.func, None) + self.assertIsNone(job.args, None) + self.assertIsNone(job.kwargs, None) + self.assertIsNone(job.origin, None) + self.assertIsNone(job.enqueued_at, None) + self.assertIsNone(job.ended_at, None) + self.assertIsNone(job.result, None) + self.assertIsNone(job.exc_info, None) + + def test_create_typical_job(self): """Creation of jobs for function calls.""" job = Job.for_call(arbitrary_function, 3, 4, z=2) # Jobs have a random UUID self.assertIsNotNone(job.id) self.assertIsNotNone(job.created_at) + self.assertIsNotNone(job.description) # Job data is set... self.assertEquals(job.func, arbitrary_function) @@ -76,8 +78,39 @@ class TestJob(RQTestCase): self.assertEquals(job.created_at, datetime(2012, 2, 7, 22, 13, 24)) - def test_persistence_of_jobs(self): - """Storing and fetching of jobs.""" + def test_persistence_of_empty_jobs(self): + """Storing empty jobs.""" + job = Job() + job.save() + + expected_date = strip_milliseconds(job.created_at) + stored_date = self.testconn.hget(job.key, 'created_at') + self.assertEquals( + times.to_universal(stored_date), + expected_date) + + # ... and no other keys are stored + self.assertItemsEqual( + self.testconn.hkeys(job.key), + ['created_at']) + + def test_persistence_of_typical_jobs(self): + """Storing typical jobs.""" + job = Job.for_call(arbitrary_function, 3, 4, z=2) + job.save() + + expected_date = strip_milliseconds(job.created_at) + stored_date = self.testconn.hget(job.key, 'created_at') + self.assertEquals( + times.to_universal(stored_date), + expected_date) + + # ... and no other keys are stored + self.assertItemsEqual( + self.testconn.hkeys(job.key), + ['created_at', 'data', 'description']) + + def test_store_then_fetch(self): job = Job.for_call(arbitrary_function, 3, 4, z=2) job.save() @@ -94,9 +127,8 @@ class TestJob(RQTestCase): with self.assertRaises(NoSuchJobError): Job.fetch('b4a44d44-da16-4620-90a6-798e8cd72ca0') - - def test_dequeue_unreadable_data(self): - """Dequeue fails on unreadable data.""" + def test_fetching_unreadable_data(self): + """Fetching fails on unreadable data.""" # Set up job = Job.for_call(arbitrary_function, 3, 4, z=2) job.save() diff --git a/tests/test_worker.py b/tests/test_worker.py index 2be9fe1..5065f83 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -1,5 +1,6 @@ from tests import RQTestCase from tests import testjob, failing_job +from tests.helpers import strip_milliseconds from rq import Queue, Worker from rq.job import Job