Merge lp:~adeuring/charmworld/more-heartbeat-info-3 into lp:charmworld

Proposed by Abel Deuring
Status: Merged
Approved by: Abel Deuring
Approved revision: 465
Merged at revision: 461
Proposed branch: lp:~adeuring/charmworld/more-heartbeat-info-3
Merge into: lp:charmworld
Diff against target: 628 lines (+364/-11)
10 files modified
charmworld/__init__.py (+5/-1)
charmworld/health.py (+51/-0)
charmworld/jobs/ingest.py (+7/-0)
charmworld/jobs/tests/test_ingest.py (+15/-6)
charmworld/tests/test_health.py (+68/-0)
charmworld/tests/test_utils.py (+111/-0)
charmworld/utils.py (+66/-0)
charmworld/views/misc.py (+6/-0)
charmworld/views/tests/test_misc.py (+34/-4)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~adeuring/charmworld/more-heartbeat-info-3
Reviewer Review Type Date Requested Status
Juju Gui Bot continuous-integration Approve
Abel Deuring (community) Approve
Benji York (community) Approve
Review via email: mp+195065@code.launchpad.net

Commit message

heartbeat page:
  - show a warning if charmworld's crontab file is missing
  - show the time when the last ingest job finished
  - show the time when the charmworld app server(s) were started

Description of the change

This branch adds three more details to the heartbeat page:

- if a crontrab file for charmworld exists. (The crontab file was
  missing in the past...)
- the time when the server was started (suggestion by rick_h)
- the time when the last ingest job finished.

The crontab check is trivial. To store the timestamps "server started"
and "last ingest job", I added a collection "status" to the mongo DB.
I am not sure if this is the right collection name -- "misc" might
be better just in case that some container for some other data is
needed.

The time of the last ingest job is added by calling the new function
set_status_timestamp() in ingest.run_job(); this required a modification
in the unit tests of run_job() and in run_job() because
set_status_timestamp() obviously needs a MongoDB instance.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

> This branch adds three more details to the heartbeat page:

Very nice. The branch looks good once you have addressed my comments to
your satisfaction.

> To store the timestamps "server started" and "last ingest job", I
> added a collection "status" to the mongo DB. I am not sure if this is
> the right collection name -- "misc" might be better just in case that
> some container for some other data is needed.

How about "server_status"? That will keep "status" available for
application data.

We happen to run a single server at the moment, but we should assume
that in as few places as possible. How about a mapping from hostname to
last start time, that way we can grow into multiple servers.

> The time of the last ingest job is added by calling the new function
> set_status_timestamp() in ingest.run_job(); this required a modification
> in the unit tests of run_job() and in run_job() because
> set_status_timestamp() obviously needs a MongoDB instance.

From line 106 of the diff:

+ # Some tests do not set the 'db' attribute.
+ if getattr(job, 'db', None) is not None:
+ set_status_timestamp(job.db, LAST_INGEST_JOB_FINISHED)

It seems that there is only one test that was faking the DB; I wonder if
introducing this code path is worth it for one test. It seems to me
that it would be preferable to mutate the test rather than the code.

Other thoughts:

The first sentence of the docstring on line 277 spans more than one
line. I would just make it a comment.

Should we check the mode bits of the crontab file? I don't know that it
has ever caused us a problem, but it is a common problem people have
with crontabs.

In formatted_status_timestamp(), it would be nice to have the time zone
on the formatted time (i.e., tack on a "Z").

I would like to see the already too big test_all_checks_pass() and
test_checks_fail() broken into one test for each passing/failing
component, but that's not a blocker.

review: Approve
459. By Abel Deuring

MongoDB collection name 'status' changed to 'server_status'

460. By Abel Deuring

timezone added in formatted_status_timestamp()

461. By Abel Deuring

call set_status_timestamp() in IngestJob.run(), not in run_job()

462. By Abel Deuring

record a server's start time together with the hostname

463. By Abel Deuring

script added to remove stale entries in the "server_started" dict.

Revision history for this message
Abel Deuring (adeuring) wrote :

On 13.11.2013 15:24, Benji York wrote:
> Review: Approve
>
>> This branch adds three more details to the heartbeat page:
>
> Very nice. The branch looks good once you have addressed my comments to
> your satisfaction.
>
>> To store the timestamps "server started" and "last ingest job", I
>> added a collection "status" to the mongo DB. I am not sure if this is
>> the right collection name -- "misc" might be better just in case that
>> some container for some other data is needed.
>
> How about "server_status"? That will keep "status" available for
> application data.

Fixed.

>
> We happen to run a single server at the moment, but we should assume
> that in as few places as possible. How about a mapping from hostname to
> last start time, that way we can grow into multiple servers.

Changed. We discussed this on IRC; ISTM that for now the best key to
identify a server is it's hostname. This is not ideal in conjunction
with juju, but it allows at least to look up a machine as seen on the
heartbeat page in "juju status".

Since hostnames may become stale, I also added a script that allows to
manually remove stale entries from the database.

>
>> The time of the last ingest job is added by calling the new function
>> set_status_timestamp() in ingest.run_job(); this required a modification
>> in the unit tests of run_job() and in run_job() because
>> set_status_timestamp() obviously needs a MongoDB instance.
>
>>From line 106 of the diff:
>
> + # Some tests do not set the 'db' attribute.
> + if getattr(job, 'db', None) is not None:
> + set_status_timestamp(job.db, LAST_INGEST_JOB_FINISHED)
>
> It seems that there is only one test that was faking the DB; I wonder if
> introducing this code path is worth it for one test. It seems to me
> that it would be preferable to mutate the test rather than the code.

Right, that's a bit ugly. More than one test is affected though:
test_run_job_defaults(), test_run_job_no_setup(),
test_run_job_db_specified() (here, the second run_job() call fails),
test_run_job_general_exception().

I moved the call of set_status_timestamp() into UpdateCharmJob.run() and
UpdateBundleJob.run(); this makes the "if getattr(...)" unnecessary.

>
> Other thoughts:
>
> The first sentence of the docstring on line 277 spans more than one
> line. I would just make it a comment.

changed.

>
> Should we check the mode bits of the crontab file? I don't know that it
> has ever caused us a problem, but it is a common problem people have
> with crontabs.
>
> In formatted_status_timestamp(), it would be nice to have the time zone
> on the formatted time (i.e., tack on a "Z").

Changed.

>
> I would like to see the already too big test_all_checks_pass() and
> test_checks_fail() broken into one test for each passing/failing
> component, but that's not a blocker.
>

Revision history for this message
Benji York (benji) wrote :

The changes look great.

Since we understand tests the best at the moment we write them, I'm a big fan of starting tests with short comments that describe what the test is about. Here's a diff that will apply some to this branch:

--- charmworld/tests/test_utils.py 2013-11-14 10:17:12.880649945 -0600
+++ charmworld/tests/test_utils.py 2013-11-14 10:42:51.832685061 -0600
@@ -1,4 +1,5 @@
     def test_remove_server_start_time_entries(self):
+ # Each time a server starts it stores the current time in the DB.
         self.db.server_status.insert({
             '_id': SERVER_STARTED,
             'foo': 123.456,
@@ -17,6 +18,8 @@
         self.assertEqual('', output.getvalue())

     def test_remove_server_start_time_entries_no_args(self):
+ # If the server start time script is run without arguments it generates
+ # an error message.
         output = StringIO()
         self.assertEqual(
             1, remove_server_start_time_entries([''], output))
@@ -25,6 +28,8 @@
             output.getvalue())

     def test_remove_server_start_time_entries_no_db_record(self):
+ # If there are no server start times yet and the script is run, it
+ # informs the user.
         output = StringIO()
         self.assertEqual(
             1, remove_server_start_time_entries(['', 'foo'], output))
@@ -32,6 +37,8 @@
             'No server start times are yet recorded.\n', output.getvalue())

     def test_remove_server_start_time_entries_invalid_server_name(self):
+ # If the script is asked to remove a server start time that does not
+ # exist, it generates an informative message.
         self.db.server_status.insert({
             '_id': SERVER_STARTED,
             'foo': 123.456,

review: Approve
464. By Abel Deuring

comments added to the new tests.

465. By Abel Deuring

trunk merged; conflicts resolved.

Revision history for this message
Abel Deuring (adeuring) :
review: Approve
Revision history for this message
Juju Gui Bot (juju-gui-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmworld/__init__.py'
--- charmworld/__init__.py 2013-09-03 19:57:54 +0000
+++ charmworld/__init__.py 2013-11-14 17:14:59 +0000
@@ -19,7 +19,10 @@
19from charmworld.models import UserMgr19from charmworld.models import UserMgr
20from charmworld.routes import build_routes20from charmworld.routes import build_routes
21from charmworld.globals import default_globals_factory21from charmworld.globals import default_globals_factory
22from charmworld.utils import get_session_url22from charmworld.utils import (
23 get_session_url,
24 record_server_start_time,
25)
2326
2427
25class cached_view_config(view_config):28class cached_view_config(view_config):
@@ -142,5 +145,6 @@
142 config.add_static_view('static', 'charmworld:static', cache_max_age=3600)145 config.add_static_view('static', 'charmworld:static', cache_max_age=3600)
143 config = build_routes(config)146 config = build_routes(config)
144 config.scan("charmworld.views")147 config.scan("charmworld.views")
148 record_server_start_time(database)
145149
146 return config.make_wsgi_app()150 return config.make_wsgi_app()
147151
=== modified file 'charmworld/health.py'
--- charmworld/health.py 2013-10-31 16:00:29 +0000
+++ charmworld/health.py 2013-11-14 17:14:59 +0000
@@ -4,7 +4,14 @@
4from bzrlib.branch import Branch4from bzrlib.branch import Branch
5from bzrlib.transport import get_transport5from bzrlib.transport import get_transport
6from collections import namedtuple6from collections import namedtuple
7import os
7from os.path import dirname8from os.path import dirname
9import time
10
11from charmworld.utils import (
12 LAST_INGEST_JOB_FINISHED,
13 SERVER_STARTED,
14)
815
916
10Check = namedtuple('check', 'name status remark')17Check = namedtuple('check', 'name status remark')
@@ -185,3 +192,47 @@
185 finally:192 finally:
186 check = Check('ElasticSearch server', status, remark)193 check = Check('ElasticSearch server', status, remark)
187 return check194 return check
195
196
197def check_crontab(request):
198 """Check that a crontab file for charmworld exists."""
199 if 'charmworld' in os.listdir('/etc/cron.d'):
200 status = PASS
201 remark = 'Crontab file for charmworld exists.'
202 else:
203 status = FAIL
204 remark = 'Crontab file for charmworld not found.'
205 return Check('Crontab', status, remark)
206
207
208def formatted_timestamp(timestamp):
209 return time.strftime('%Y-%m-%d %H:%M:%SZ', time.gmtime(timestamp))
210
211
212def check_server_start_time(request):
213 """Return the time when the server was last started."""
214 try:
215 start_times = request.db.server_status.find_one(SERVER_STARTED)
216 del start_times['_id']
217 start_times = [
218 '%s: %s' % (hostname, formatted_timestamp(timestamp))
219 for hostname, timestamp in sorted(start_times.items())]
220 remark = ', '.join(start_times)
221 status = PASS
222 except Exception:
223 remark = "Can't retrieve the server's start time."
224 status = FAIL
225 return Check('Server started', status, remark)
226
227
228def check_last_ingest_job(request):
229 """Return the time when the ingest job finished."""
230 try:
231 timestamp = request.db.server_status.find_one(
232 LAST_INGEST_JOB_FINISHED)['time']
233 remark = 'finished at %s' % formatted_timestamp(timestamp)
234 status = PASS
235 except Exception:
236 remark = "Can't retrieve the time when the last ingest job finished."
237 status = FAIL
238 return Check('Last ingest job', status, remark)
188239
=== modified file 'charmworld/jobs/ingest.py'
--- charmworld/jobs/ingest.py 2013-11-06 22:54:45 +0000
+++ charmworld/jobs/ingest.py 2013-11-14 17:14:59 +0000
@@ -51,9 +51,11 @@
51 SearchServiceNotAvailable,51 SearchServiceNotAvailable,
52)52)
53from charmworld.utils import (53from charmworld.utils import (
54 LAST_INGEST_JOB_FINISHED,
54 quote_key,55 quote_key,
55 quote_yaml,56 quote_yaml,
56 read_locked,57 read_locked,
58 set_status_timestamp,
57 timestamp,59 timestamp,
58 unquote_yaml,60 unquote_yaml,
59)61)
@@ -95,6 +97,9 @@
95 def run(self, charm_data):97 def run(self, charm_data):
96 raise NotImplementedError98 raise NotImplementedError
9799
100 def update_ingest_status(self):
101 set_status_timestamp(self.db, LAST_INGEST_JOB_FINISHED)
102
98103
99class DBIngestJob(IngestJob):104class DBIngestJob(IngestJob):
100105
@@ -303,6 +308,7 @@
303 CharmSource(self.db, index_client).save(charm_data)308 CharmSource(self.db, index_client).save(charm_data)
304 else:309 else:
305 self.log.info('Skipping %s' % charm_data['_id'])310 self.log.info('Skipping %s' % charm_data['_id'])
311 self.update_ingest_status()
306312
307313
308class UpdateBundleJob(DBIngestJob):314class UpdateBundleJob(DBIngestJob):
@@ -374,6 +380,7 @@
374 basket_data['name_revno'],380 basket_data['name_revno'],
375 basket_data['first_change'], basket_data['last_change'],381 basket_data['first_change'], basket_data['last_change'],
376 basket_data['changes'], basket_data['branch_spec'])382 basket_data['changes'], basket_data['branch_spec'])
383 self.update_ingest_status()
377384
378385
379def _rev_info(r, branch):386def _rev_info(r, branch):
380387
=== modified file 'charmworld/jobs/tests/test_ingest.py'
--- charmworld/jobs/tests/test_ingest.py 2013-11-06 23:04:32 +0000
+++ charmworld/jobs/tests/test_ingest.py 2013-11-14 17:14:59 +0000
@@ -12,6 +12,7 @@
12import shutil12import shutil
13import subprocess13import subprocess
14from textwrap import dedent14from textwrap import dedent
15import time
1516
16import bzrlib17import bzrlib
17from bzrlib.bzrdir import BzrDir18from bzrlib.bzrdir import BzrDir
@@ -53,6 +54,7 @@
53 TestCase,54 TestCase,
54)55)
55from charmworld.utils import (56from charmworld.utils import (
57 LAST_INGEST_JOB_FINISHED,
56 quote_key,58 quote_key,
57)59)
5860
@@ -688,9 +690,11 @@
688 if self.exc is not None:690 if self.exc is not None:
689 raise self.exc('test error')691 raise self.exc('test error')
690 self.run_called = True692 self.run_called = True
691693 if getattr(self, 'db', None) is not None:
692694 self.update_ingest_status()
693class TestRunJob(TestCase):695
696
697class TestRunJob(MongoTestBase):
694698
695 def test_run_job_defaults(self):699 def test_run_job_defaults(self):
696 job = TestJob()700 job = TestJob()
@@ -708,10 +712,15 @@
708712
709 def test_run_job_db_specified(self):713 def test_run_job_db_specified(self):
710 job = TestJob()714 job = TestJob()
711 run_job(job, 'foo', db='db')715 before = time.time()
712 self.assertEqual('db', job.db)716 run_job(job, 'foo', db=self.db)
717 after = time.time()
718 self.assertEqual(self.db, job.db)
719 job_finished = self.db.server_status.find_one(LAST_INGEST_JOB_FINISHED)
720 job_finished = job_finished['time']
721 self.assertTrue(before <= job_finished <= after)
713 job = TestJob()722 job = TestJob()
714 run_job(job, 'foo', needs_setup=False, db='db')723 run_job(job, 'foo', needs_setup=False, db=self.db)
715 self.assertIs(None, getattr(job, 'db', None))724 self.assertIs(None, getattr(job, 'db', None))
716725
717 def test_run_job_general_exception(self):726 def test_run_job_general_exception(self):
718727
=== modified file 'charmworld/tests/test_health.py'
--- charmworld/tests/test_health.py 2013-10-31 16:00:29 +0000
+++ charmworld/tests/test_health.py 2013-11-14 17:14:59 +0000
@@ -2,6 +2,8 @@
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4from mock import patch4from mock import patch
5import os
6import re
57
6from charmworld.health import (8from charmworld.health import (
7 check_api2,9 check_api2,
@@ -11,14 +13,22 @@
11 check_charms_collection,13 check_charms_collection,
12 check_collections,14 check_collections,
13 check_charm_index,15 check_charm_index,
16 check_crontab,
14 check_elasticsearch_status,17 check_elasticsearch_status,
15 check_ingest_queues,18 check_ingest_queues,
19 check_last_ingest_job,
20 check_server_start_time,
16)21)
17from charmworld.models import FeaturedSource22from charmworld.models import FeaturedSource
18from charmworld.testing import (23from charmworld.testing import (
19 factory,24 factory,
20 ViewTestBase,25 ViewTestBase,
21)26)
27from charmworld.utils import (
28 LAST_INGEST_JOB_FINISHED,
29 record_server_start_time,
30 set_status_timestamp,
31)
2232
2333
24class TestCheckMixin:34class TestCheckMixin:
@@ -261,3 +271,61 @@
261 check = check_elasticsearch_status(request)271 check = check_elasticsearch_status(request)
262 remark = "Can't retrieve the ES server's health status."272 remark = "Can't retrieve the ES server's health status."
263 self.assertCheck(check, 'ElasticSearch server', 'Fail', remark)273 self.assertCheck(check, 'ElasticSearch server', 'Fail', remark)
274
275 def test_check_crontab_pass(self):
276 def make_fake_listdir():
277 def fake_listdir(path):
278 return ('foo', 'charmworld')
279 return fake_listdir
280
281 with patch.object(os, 'listdir', new_callable=make_fake_listdir):
282 request = self.getRequest()
283 check = check_crontab(request)
284 remark = 'Crontab file for charmworld exists.'
285 self.assertCheck(check, 'Crontab', 'Pass', remark)
286
287 def test_check_crontab_fail(self):
288 def make_fake_listdir():
289 def fake_listdir(path):
290 return ('foo', )
291 return fake_listdir
292
293 with patch.object(os, 'listdir', new_callable=make_fake_listdir):
294 request = self.getRequest()
295 check = check_crontab(request)
296 remark = 'Crontab file for charmworld not found.'
297 self.assertCheck(check, 'Crontab', 'Fail', remark)
298
299 def test_check_server_start_time_pass(self):
300 def make_uname():
301 def uname():
302 return (None, 'foo')
303 return uname
304
305 with patch.object(os, 'uname', new_callable=make_uname):
306 record_server_start_time(self.db)
307 check = check_server_start_time(self.getRequest())
308 self.assertCheck(check, 'Server started', 'Pass')
309 mo = re.search(
310 '^foo: \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\dZ$', check.remark)
311 self.assertIsNot(None, mo)
312
313 def test_check_server_start_time_fail(self):
314 check = check_server_start_time(self.getRequest())
315 self.assertCheck(
316 check, 'Server started', 'Fail',
317 "Can't retrieve the server's start time.")
318
319 def test_check_last_ingest_job_pass(self):
320 set_status_timestamp(self.db, LAST_INGEST_JOB_FINISHED)
321 check = check_last_ingest_job(self.getRequest())
322 self.assertCheck(check, 'Last ingest job', 'Pass')
323 mo = re.search(
324 '^finished at \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\dZ$', check.remark)
325 self.assertIsNot(None, mo)
326
327 def test_check_last_ingest_job_fail(self):
328 check = check_last_ingest_job(self.getRequest())
329 self.assertCheck(
330 check, 'Last ingest job', 'Fail',
331 "Can't retrieve the time when the last ingest job finished.")
264332
=== modified file 'charmworld/tests/test_utils.py'
--- charmworld/tests/test_utils.py 2013-11-13 17:40:11 +0000
+++ charmworld/tests/test_utils.py 2013-11-14 17:14:59 +0000
@@ -13,6 +13,7 @@
13)13)
14import tempfile14import tempfile
15from textwrap import dedent15from textwrap import dedent
16import time
16from unittest import TestCase17from unittest import TestCase
17import urllib18import urllib
1819
@@ -29,6 +30,10 @@
29 pretty_timedelta,30 pretty_timedelta,
30 quote_key,31 quote_key,
31 quote_yaml,32 quote_yaml,
33 record_server_start_time,
34 remove_server_start_time_entries,
35 SERVER_STARTED,
36 set_status_timestamp,
32 unquote_yaml,37 unquote_yaml,
33 _process_dict_keys,38 _process_dict_keys,
34)39)
@@ -364,3 +369,109 @@
364 self.assertEqual(369 self.assertEqual(
365 build_metric_key('metric', 'foo/bar/42'),370 build_metric_key('metric', 'foo/bar/42'),
366 'metric/foo/bar/42')371 'metric/foo/bar/42')
372
373
374class TestSetStatusTimeStamp(MongoTestBase):
375
376 def test_set_status_timestamp(self):
377 # set_status_timestamp() records the current time in a MongoDB
378 # record.
379 before = time.time()
380 set_status_timestamp(self.db, 'foo')
381 after = time.time()
382 recorded = self.db.server_status.find_one('foo')['time']
383 self.assertTrue(before <= recorded <= after)
384
385
386class TestServerStartTime(MongoTestBase):
387
388 def test_record_server_start_time(self):
389 # record_server_start_time() records the hostname and the current
390 # time in a MongoDB record.
391 import os
392
393 def make_uname(hostname):
394 def uname():
395 return (None, hostname)
396 return uname
397
398 with patch.object(
399 os, 'uname', new_callable=make_uname,
400 hostname='foo'):
401 before = time.time()
402 record_server_start_time(self.db)
403 after = time.time()
404 foo_start_time = self.db.server_status.find_one(SERVER_STARTED)['foo']
405 self.assertTrue(before <= foo_start_time <= after)
406
407 # Updating a server's start time retains already recorded data.
408 with patch.object(
409 os, 'uname', new_callable=make_uname,
410 hostname='bar'):
411 record_server_start_time(self.db)
412 recorded = self.db.server_status.find_one(SERVER_STARTED)
413 self.assertEqual(set(('_id', 'foo', 'bar')), set(recorded))
414
415 # Existing records are updated.
416 with patch.object(
417 os, 'uname', new_callable=make_uname,
418 hostname='foo'):
419 before = time.time()
420 record_server_start_time(self.db)
421 after = time.time()
422 recorded = self.db.server_status.find_one(SERVER_STARTED)
423 self.assertTrue(recorded['foo'] > foo_start_time)
424
425 def test_remove_server_start_time_entries(self):
426 # Each time a server starts it stores the current time in the DB.
427 # Stale entries can be removed by calling
428 # remove_server_start_time_entries() (used in the script
429 # remove-start-time-entry).
430 self.db.server_status.insert({
431 '_id': SERVER_STARTED,
432 'foo': 123.456,
433 'bar': 234.567,
434 })
435 output = StringIO()
436 self.assertEqual(
437 0, remove_server_start_time_entries(['', 'foo'], output))
438 start_times = self.db.server_status.find_one({'_id': SERVER_STARTED})
439 self.assertEqual(
440 {
441 '_id': SERVER_STARTED,
442 'bar': 234.567,
443 },
444 start_times)
445 self.assertEqual('', output.getvalue())
446
447 def test_remove_server_start_time_entries_no_args(self):
448 # If the server start time script is run without arguments it
449 # generates an error message.
450 output = StringIO()
451 self.assertEqual(
452 1, remove_server_start_time_entries([''], output))
453 self.assertEqual(
454 'At least one server hostname must be specified.\n',
455 output.getvalue())
456
457 def test_remove_server_start_time_entries_no_db_record(self):
458 # If there are no server start times yet and the script is run, it
459 # informs the user.
460 output = StringIO()
461 self.assertEqual(
462 1, remove_server_start_time_entries(['', 'foo'], output))
463 self.assertEqual(
464 'No server start times are yet recorded.\n', output.getvalue())
465
466 def test_remove_server_start_time_entries_invalid_server_name(self):
467 # If the script is asked to remove a server start time that does not
468 # exist, it generates an informative message.
469 self.db.server_status.insert({
470 '_id': SERVER_STARTED,
471 'foo': 123.456,
472 })
473 output = StringIO()
474 self.assertEqual(
475 1, remove_server_start_time_entries(['', 'bar'], output))
476 self.assertEqual(
477 'hostname bar not found.\n', output.getvalue())
367478
=== modified file 'charmworld/utils.py'
--- charmworld/utils.py 2013-11-13 19:37:19 +0000
+++ charmworld/utils.py 2013-11-14 17:14:59 +0000
@@ -11,6 +11,7 @@
11import logging.config11import logging.config
12import os12import os
13import re13import re
14import sys
14import time15import time
15from urllib import unquote16from urllib import unquote
16import urlparse17import urlparse
@@ -28,6 +29,12 @@
28 (60, '%d minute')29 (60, '%d minute')
29)30)
3031
32# The MongoDB ID of the time the last ingest job finished in the 'status'
33# collection.
34LAST_INGEST_JOB_FINISHED = 'last_ingest_job_finished'
35# The MongoDB ID of the time the server was started in the 'status' collection.
36SERVER_STARTED = 'server_started'
37
3138
32def prettify_timedelta(diff):39def prettify_timedelta(diff):
33 """Return a prettified time delta, suitable for pretty printing."""40 """Return a prettified time delta, suitable for pretty printing."""
@@ -227,3 +234,62 @@
227 # Metrics are stored for the versionless ID.234 # Metrics are stored for the versionless ID.
228 versionless_id = id_revision_regex.sub('', item_id)235 versionless_id = id_revision_regex.sub('', item_id)
229 return '/'.join([metric_name, versionless_id])236 return '/'.join([metric_name, versionless_id])
237
238
239def set_status_timestamp(database, id):
240 database.server_status.save({
241 '_id': id,
242 'time': time.time(),
243 })
244
245
246def record_server_start_time(database):
247 hostname = os.uname()[1]
248 database.server_status.update(
249 {
250 '_id': SERVER_STARTED,
251 },
252 {
253 '$set': {
254 hostname: time.time(),
255 },
256 },
257 upsert=True)
258
259
260def remove_server_start_time_entries(argv, output, db=None):
261 from charmworld.models import getconnection
262 from charmworld.models import getdb
263
264 if db is None:
265 settings = get_ini()
266 connection = getconnection(settings)
267 db = getdb(connection, settings.get('mongo.database'))
268
269 if len(argv) < 2:
270 print >>output, "At least one server hostname must be specified."
271 return 1
272
273 server_start_times = db.server_status.find_one({'_id': SERVER_STARTED})
274 if server_start_times is None:
275 print >>output, "No server start times are yet recorded."
276 return 1
277
278 for name in argv[1:]:
279 if name not in server_start_times:
280 print >>output, "hostname %s not found." % name
281 return 1
282
283 for name in argv[1:]:
284 db.server_status.update(
285 {
286 '_id': SERVER_STARTED,
287 },
288 {
289 '$unset': {name: ''},
290 })
291 return 0
292
293
294def remove_server_start_time():
295 sys.exit(remove_server_start_time_entries(sys.argv, sys.stderr))
230296
=== modified file 'charmworld/views/misc.py'
--- charmworld/views/misc.py 2013-10-31 14:10:57 +0000
+++ charmworld/views/misc.py 2013-11-14 17:14:59 +0000
@@ -14,8 +14,11 @@
14 check_charm_index,14 check_charm_index,
15 check_charms_collection,15 check_charms_collection,
16 check_collections,16 check_collections,
17 check_crontab,
17 check_elasticsearch_status,18 check_elasticsearch_status,
18 check_ingest_queues,19 check_ingest_queues,
20 check_last_ingest_job,
21 check_server_start_time,
19)22)
2023
2124
@@ -62,4 +65,7 @@
62 checks.append(check_ingest_queues(request))65 checks.append(check_ingest_queues(request))
63 checks.append(check_bzr_revision(request))66 checks.append(check_bzr_revision(request))
64 checks.append(check_elasticsearch_status(request))67 checks.append(check_elasticsearch_status(request))
68 checks.append(check_crontab(request))
69 checks.append(check_server_start_time(request))
70 checks.append(check_last_ingest_job(request))
65 return {'checks': checks}71 return {'checks': checks}
6672
=== modified file 'charmworld/views/tests/test_misc.py'
--- charmworld/views/tests/test_misc.py 2013-11-11 10:44:53 +0000
+++ charmworld/views/tests/test_misc.py 2013-11-14 17:14:59 +0000
@@ -17,6 +17,11 @@
17 WebTestBase,17 WebTestBase,
18)18)
19from charmworld.tests.test_health import TestCheckMixin19from charmworld.tests.test_health import TestCheckMixin
20from charmworld.utils import (
21 LAST_INGEST_JOB_FINISHED,
22 record_server_start_time,
23 set_status_timestamp,
24)
20from charmworld.views.misc import heartbeat25from charmworld.views.misc import heartbeat
2126
2227
@@ -119,6 +124,8 @@
119 self.use_index_client()124 self.use_index_client()
120 self.index_client.index_charm(charm_data)125 self.index_client.index_charm(charm_data)
121 FeaturedSource.from_db(self.db).set_featured(charm_data, 'charm')126 FeaturedSource.from_db(self.db).set_featured(charm_data, 'charm')
127 record_server_start_time(self.db)
128 set_status_timestamp(self.db, LAST_INGEST_JOB_FINISHED)
122129
123 def make_all_collection_names():130 def make_all_collection_names():
124 def all_collection_names():131 def all_collection_names():
@@ -127,13 +134,19 @@
127 'bundles', 'charm-queue', 'charms']134 'bundles', 'charm-queue', 'charms']
128 return all_collection_names135 return all_collection_names
129136
137 def make_fake_listdir():
138 def fake_listdir(path):
139 return ('foo', 'charmworld')
140 return fake_listdir
141
130 request = self.getRequest()142 request = self.getRequest()
131 with patch.object(request.db, 'collection_names',143 with patch.object(request.db, 'collection_names',
132 new_callable=make_all_collection_names):144 new_callable=make_all_collection_names):
133 response = heartbeat(request)145 with patch.object(os, 'listdir', new_callable=make_fake_listdir):
146 response = heartbeat(request)
134147
135 checks = response['checks']148 checks = response['checks']
136 self.assertEqual(9, len(checks))149 self.assertEqual(12, len(checks))
137 remark = '1 charms found'150 remark = '1 charms found'
138 self.assertCheck(checks[0], 'charms collection', 'Pass', remark)151 self.assertCheck(checks[0], 'charms collection', 'Pass', remark)
139 remark = '1 bundles found'152 remark = '1 bundles found'
@@ -149,17 +162,28 @@
149 self.assertCheck(checks[6], 'Ingest queue sizes', 'Pass', remark)162 self.assertCheck(checks[6], 'Ingest queue sizes', 'Pass', remark)
150 self.assertCheck(checks[7], 'BZR revision', 'Pass')163 self.assertCheck(checks[7], 'BZR revision', 'Pass')
151 self.assertCheck(checks[8], 'ElasticSearch server', 'Pass')164 self.assertCheck(checks[8], 'ElasticSearch server', 'Pass')
165 self.assertCheck(checks[9], 'Crontab', 'Pass')
166 self.assertCheck(checks[10], 'Server started', 'Pass')
167 self.assertCheck(checks[11], 'Last ingest job', 'Pass')
152168
153 def test_checks_fail(self):169 def test_checks_fail(self):
154 # When services or data are not available, the checks fail.170 # When services or data are not available, the checks fail.
155 # No setup is performed, creating a case where services and data171 # No setup is performed, creating a case where services and data
156 # are not available172 # are not available
157 from charmworld import health173 from charmworld import health
174
175 def make_fake_listdir():
176 def fake_listdir(path):
177 return ('foo', )
178 return fake_listdir
179
158 with patch.object(health, 'get_transport',180 with patch.object(health, 'get_transport',
159 new_callable=self.makeFailCallable):181 new_callable=self.makeFailCallable):
160 response = heartbeat(self.getRequest())182 with patch.object(os, 'listdir', new_callable=make_fake_listdir):
183 response = heartbeat(self.getRequest())
184
161 checks = response['checks']185 checks = response['checks']
162 self.assertEqual(9, len(checks))186 self.assertEqual(12, len(checks))
163 remark = 'There are no charms. Is ingest running?'187 remark = 'There are no charms. Is ingest running?'
164 self.assertCheck(checks[0], 'charms collection', 'Fail', remark)188 self.assertCheck(checks[0], 'charms collection', 'Fail', remark)
165 remark = 'There are no bundles. Is ingest running?'189 remark = 'There are no bundles. Is ingest running?'
@@ -180,3 +204,9 @@
180 self.assertCheck(checks[7], 'BZR revision', 'Fail', remark)204 self.assertCheck(checks[7], 'BZR revision', 'Fail', remark)
181 remark = "Can't retrieve the ES server's health status."205 remark = "Can't retrieve the ES server's health status."
182 self.assertCheck(checks[8], 'ElasticSearch server', 'Fail', remark)206 self.assertCheck(checks[8], 'ElasticSearch server', 'Fail', remark)
207 remark = 'Crontab file for charmworld not found.'
208 self.assertCheck(checks[9], 'Crontab', 'Fail', remark)
209 remark = "Can't retrieve the server's start time."
210 self.assertCheck(checks[10], 'Server started', 'Fail', remark)
211 remark = "Can't retrieve the time when the last ingest job finished."
212 self.assertCheck(checks[11], 'Last ingest job', 'Fail', remark)
183213
=== modified file 'setup.py'
--- setup.py 2013-08-05 20:16:49 +0000
+++ setup.py 2013-11-14 17:14:59 +0000
@@ -39,6 +39,7 @@
39 migrations = charmworld.migrations.migrate:main39 migrations = charmworld.migrations.migrate:main
40 es-update = charmworld.search:update_main40 es-update = charmworld.search:update_main
41 sync-index = charmworld.models:sync_index41 sync-index = charmworld.models:sync_index
42 remove-start-time-entry = charmworld.utils:remove_server_start_time
42 [beaker.backends]43 [beaker.backends]
43 mongodb = mongodb_beaker:MongoDBNamespaceManager44 mongodb = mongodb_beaker:MongoDBNamespaceManager
44 """,45 """,

Subscribers

People subscribed via source and target branches

to all changes: