Merge lp:~lifeless/python-oops-datedir-repo/bug-971255 into lp:python-oops-datedir-repo

Proposed by Robert Collins
Status: Merged
Merged at revision: 42
Proposed branch: lp:~lifeless/python-oops-datedir-repo/bug-971255
Merge into: lp:python-oops-datedir-repo
Diff against target: 590 lines (+26/-440)
7 files modified
NEWS (+5/-0)
README (+1/-4)
oops_datedir_repo/repository.py (+17/-30)
oops_datedir_repo/tests/__init__.py (+0/-1)
oops_datedir_repo/tests/test_repository.py (+3/-33)
oops_datedir_repo/tests/test_uniquefileallocator.py (+0/-160)
oops_datedir_repo/uniquefileallocator.py (+0/-212)
To merge this branch: bzr merge lp:~lifeless/python-oops-datedir-repo/bug-971255
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+126386@code.launchpad.net

Description of the change

UniqueFileAllocator lead to lots of race-condition related pain, we've had ages now for everyone to migrate, and the previous release is entirely usable for folk that might not be ready yet. Wake up, time to die.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2012-09-19 02:21:51 +0000
+++ NEWS 2012-09-26 06:41:19 +0000
@@ -6,6 +6,11 @@
6NEXT6NEXT
7----7----
88
9* The legacy uniquefileallocator code path has been dropped, fixing
10 the bugs associated with its flawed design. As a result the
11 instance_id parameter to DateDirRepo.__init__ has been dropped.
12 (Robert Collins, #971255)
13
90.0.19140.0.19
10------15------
1116
1217
=== modified file 'README'
--- README 2011-11-11 04:48:53 +0000
+++ README 2012-09-26 06:41:19 +0000
@@ -53,14 +53,11 @@
53functions : an OOPS report can be written to a disk file via the53functions : an OOPS report can be written to a disk file via the
54serializer_rfc822.write() function, and read via the matching read() function.54serializer_rfc822.write() function, and read via the matching read() function.
5555
56The uniquefileallocator module is used by the repository implementation and
57provides a system for allocating file names on disk.
58
59Typical usage::56Typical usage::
6057
61 >>> config = oops.Config()58 >>> config = oops.Config()
62 >>> with fixtures.TempDir() as tempdir:59 >>> with fixtures.TempDir() as tempdir:
63 ... repo = oops_datedir_repo.DateDirRepo('/tmp/demo', 'servername')60 ... repo = oops_datedir_repo.DateDirRepo('/tmp/demo')
64 ... config.publishers.append(repo.publish)61 ... config.publishers.append(repo.publish)
65 ... ids = config.publish({'oops': '!!!'})62 ... ids = config.publish({'oops': '!!!'})
6663
6764
=== modified file 'oops_datedir_repo/repository.py'
--- oops_datedir_repo/repository.py 2012-09-03 23:13:20 +0000
+++ oops_datedir_repo/repository.py 2012-09-26 06:41:19 +0000
@@ -34,7 +34,6 @@
34import anybson as bson34import anybson as bson
35import serializer35import serializer
36import serializer_bson36import serializer_bson
37from uniquefileallocator import UniqueFileAllocator
3837
3938
40class DateDirRepo:39class DateDirRepo:
@@ -50,21 +49,20 @@
50 more OOPS reports. OOPS file names can take various forms, but must not49 more OOPS reports. OOPS file names can take various forms, but must not
51 end in .tmp - those are considered to be OOPS reports that are currently50 end in .tmp - those are considered to be OOPS reports that are currently
52 being written.51 being written.
52
53 * The behaviour of this class is to assign OOPS file names by hashing the
54 serialized OOPS to get a unique file name. Other naming schemes are
55 valid - the code doesn't assume anything other than the .tmp limitation
56 above.
53 """57 """
5458
55 def __init__(self, error_dir, instance_id=None, serializer=None,59 def __init__(self, error_dir, serializer=None, inherit_id=False,
56 inherit_id=False, stash_path=False):60 stash_path=False):
57 """Create a DateDirRepo.61 """Create a DateDirRepo.
5862
59 :param error_dir: The base directory to write OOPSes into. OOPSes are63 :param error_dir: The base directory to write OOPSes into. OOPSes are
60 written into a subdirectory this named after the date (e.g.64 written into a subdirectory this named after the date (e.g.
61 2011-12-30).65 2011-12-30).
62 :param instance_id: If None, OOPS file names are named after the OOPS
63 id which is generated by hashing the serialized OOPS (without the
64 id field). Otherwise OOPS file names and ids are created by
65 allocating file names through a UniqueFileAllocator.
66 UniqueFileAllocator has significant performance and concurrency
67 limits and hash based naming is recommended.
68 :param serializer: If supplied should be the module (e.g.66 :param serializer: If supplied should be the module (e.g.
69 oops_datedir_repo.serializer_rfc822) to use to serialize OOPSes.67 oops_datedir_repo.serializer_rfc822) to use to serialize OOPSes.
70 Defaults to using serializer_bson.68 Defaults to using serializer_bson.
@@ -75,14 +73,6 @@
75 It is not stored in the OOPS written to disk, only the in-memory73 It is not stored in the OOPS written to disk, only the in-memory
76 model.74 model.
77 """75 """
78 if instance_id is not None:
79 self.log_namer = UniqueFileAllocator(
80 output_root=error_dir,
81 log_type="OOPS",
82 log_subtype=instance_id,
83 )
84 else:
85 self.log_namer = None
86 self.root = error_dir76 self.root = error_dir
87 if serializer is None:77 if serializer is None:
88 serializer = serializer_bson78 serializer = serializer_bson
@@ -114,19 +104,16 @@
114 # Don't mess with the original report when changing ids etc.104 # Don't mess with the original report when changing ids etc.
115 original_report = report105 original_report = report
116 report = dict(report)106 report = dict(report)
117 if self.log_namer is not None:107 md5hash = md5(serializer_bson.dumps(report)).hexdigest()
118 oopsid, filename = self.log_namer.newId(now)108 oopsid = 'OOPS-%s' % md5hash
119 else:109 prefix = os.path.join(self.root, now.strftime('%Y-%m-%d'))
120 md5hash = md5(serializer_bson.dumps(report)).hexdigest()110 if not os.path.isdir(prefix):
121 oopsid = 'OOPS-%s' % md5hash111 os.makedirs(prefix)
122 prefix = os.path.join(self.root, now.strftime('%Y-%m-%d'))112 # For directories we need to set the x bits too.
123 if not os.path.isdir(prefix):113 os.chmod(
124 os.makedirs(prefix)114 prefix, wanted_file_permission | stat.S_IXUSR | stat.S_IXGRP |
125 # For directories we need to set the x bits too.115 stat.S_IXOTH)
126 os.chmod(116 filename = os.path.join(prefix, oopsid)
127 prefix, wanted_file_permission | stat.S_IXUSR | stat.S_IXGRP |
128 stat.S_IXOTH)
129 filename = os.path.join(prefix, oopsid)
130 if self.inherit_id:117 if self.inherit_id:
131 oopsid = report.get('id') or oopsid118 oopsid = report.get('id') or oopsid
132 report['id'] = oopsid119 report['id'] = oopsid
133120
=== modified file 'oops_datedir_repo/tests/__init__.py'
--- oops_datedir_repo/tests/__init__.py 2011-11-11 04:21:05 +0000
+++ oops_datedir_repo/tests/__init__.py 2012-09-26 06:41:19 +0000
@@ -21,7 +21,6 @@
21def test_suite():21def test_suite():
22 test_mod_names = [22 test_mod_names = [
23 'repository',23 'repository',
24 'uniquefileallocator',
25 'serializer',24 'serializer',
26 'serializer_bson',25 'serializer_bson',
27 'serializer_rfc822',26 'serializer_rfc822',
2827
=== modified file 'oops_datedir_repo/tests/test_repository.py'
--- oops_datedir_repo/tests/test_repository.py 2012-09-03 21:04:20 +0000
+++ oops_datedir_repo/tests/test_repository.py 2012-09-26 06:41:19 +0000
@@ -38,7 +38,6 @@
38 DateDirRepo,38 DateDirRepo,
39 serializer_bson,39 serializer_bson,
40 )40 )
41from oops_datedir_repo.uniquefileallocator import UniqueFileAllocator
4241
4342
44class HasUnixPermissions:43class HasUnixPermissions:
@@ -73,27 +72,6 @@
7372
74class TestDateDirRepo(testtools.TestCase):73class TestDateDirRepo(testtools.TestCase):
7574
76 def test_publish_permissions_lognamer(self):
77 errordir = self.useFixture(TempDir()).path
78 repo = DateDirRepo(errordir, 'T')
79 report = {'id': 'OOPS-91T1'}
80 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=utc)
81
82 # Set up default file creation mode to rwx------ as some restrictive
83 # servers do.
84 self.useFixture(UMaskFixture(stat.S_IRWXG | stat.S_IRWXO))
85 repo.publish(report, now)
86
87 errorfile = os.path.join(repo.log_namer.output_dir(now), '01800.T1')
88 # Check errorfile and directory are set with the correct permission:
89 # rw-r--r-- and rwxr-xr-x accordingly
90 file_perms = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
91 has_file_permission = HasUnixPermissions(file_perms)
92 has_dir_permission = HasUnixPermissions(
93 file_perms | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
94 self.assertThat(errorfile, has_file_permission)
95 self.assertThat(repo.log_namer.output_dir(now), has_dir_permission)
96
97 def test_publish_permissions_hashnames(self):75 def test_publish_permissions_hashnames(self):
98 repo = DateDirRepo(self.useFixture(TempDir()).path, stash_path=True)76 repo = DateDirRepo(self.useFixture(TempDir()).path, stash_path=True)
99 report = {'id': 'OOPS-91T1'}77 report = {'id': 'OOPS-91T1'}
@@ -112,23 +90,15 @@
112 self.assertThat(report['datedir_repo_filepath'], has_file_permission)90 self.assertThat(report['datedir_repo_filepath'], has_file_permission)
113 self.assertThat(repo.root + '/2006-04-01', has_dir_permission)91 self.assertThat(repo.root + '/2006-04-01', has_dir_permission)
11492
115 def test_sets_log_namer_to_a_UniqueFileAllocator(self):
116 repo = DateDirRepo(self.useFixture(TempDir()).path, 'T')
117 self.assertIsInstance(repo.log_namer, UniqueFileAllocator)
118
119 def test_default_serializer_bson(self):93 def test_default_serializer_bson(self):
120 repo = DateDirRepo(self.useFixture(TempDir()).path, 'T')94 repo = DateDirRepo(self.useFixture(TempDir()).path)
121 self.assertEqual(serializer_bson, repo.serializer)95 self.assertEqual(serializer_bson, repo.serializer)
12296
123 def test_settable_serializer(self):97 def test_settable_serializer(self):
124 an_object = object()98 an_object = object()
125 repo = DateDirRepo(self.useFixture(TempDir()).path, 'T', an_object)99 repo = DateDirRepo(self.useFixture(TempDir()).path, an_object)
126 self.assertEqual(an_object, repo.serializer)100 self.assertEqual(an_object, repo.serializer)
127101
128 def test_no_instance_id_no_log_namer(self):
129 repo = DateDirRepo(self.useFixture(TempDir()).path)
130 self.assertEqual(None, repo.log_namer)
131
132 def test_publish_via_hash(self):102 def test_publish_via_hash(self):
133 repo = DateDirRepo(self.useFixture(TempDir()).path)103 repo = DateDirRepo(self.useFixture(TempDir()).path)
134 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=utc)104 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=utc)
@@ -178,7 +148,7 @@
178148
179 def test_publish_existing_id_lognamer(self):149 def test_publish_existing_id_lognamer(self):
180 # The id reuse and file allocation strategies should be separate.150 # The id reuse and file allocation strategies should be separate.
181 repo = DateDirRepo(self.useFixture(TempDir()).path, 'X',151 repo = DateDirRepo(self.useFixture(TempDir()).path,
182 inherit_id=True, stash_path=True)152 inherit_id=True, stash_path=True)
183 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=utc)153 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=utc)
184 report = {'time': now, 'id': '45'}154 report = {'time': now, 'id': '45'}
185155
=== removed file 'oops_datedir_repo/tests/test_uniquefileallocator.py'
--- oops_datedir_repo/tests/test_uniquefileallocator.py 2011-11-11 04:21:05 +0000
+++ oops_datedir_repo/tests/test_uniquefileallocator.py 1970-01-01 00:00:00 +0000
@@ -1,160 +0,0 @@
1# Copyright (c) 2010, 2011, Canonical Ltd
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published by
5# the Free Software Foundation, version 3 only.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU Lesser General Public License for more details.
11#
12# You should have received a copy of the GNU Lesser General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14# GNU Lesser General Public License version 3 (see the file LICENSE).
15
16"""Tests for the unique file naming facility."""
17
18__metaclass__ = type
19
20import datetime
21import os
22import stat
23
24from fixtures import TempDir
25import pytz
26import testtools
27
28from oops_datedir_repo.uniquefileallocator import UniqueFileAllocator
29
30
31UTC = pytz.timezone('UTC')
32
33
34class TestUniqueFileAllocator(testtools.TestCase):
35
36 def setUp(self):
37 super(TestUniqueFileAllocator, self).setUp()
38 tempdir = self.useFixture(TempDir())
39 self._tempdir = tempdir.path
40
41 def test_setToken(self):
42 namer = UniqueFileAllocator("/any-old/path/", 'OOPS', 'T')
43 self.assertEqual('T', namer.get_log_infix())
44
45 # Some scripts will append a string token to the prefix.
46 namer.setToken('CW')
47 self.assertEqual('TCW', namer.get_log_infix())
48
49 # Some scripts run multiple processes and append a string number
50 # to the prefix.
51 namer.setToken('1')
52 self.assertEqual('T1', namer.get_log_infix())
53
54 def assertUniqueFileAllocator(self, namer, now, expected_id,
55 expected_last_id, expected_suffix, expected_lastdir):
56 logid, filename = namer.newId(now)
57 self.assertEqual(logid, expected_id)
58 self.assertEqual(filename,
59 os.path.join(namer._output_root, expected_suffix))
60 self.assertEqual(namer._last_serial, expected_last_id)
61 self.assertEqual(namer._last_output_dir,
62 os.path.join(namer._output_root, expected_lastdir))
63
64 def test_newId(self):
65 # TODO: This should return an id, fileobj instead of a file name, to
66 # reduce races with threads that are slow to use what they asked for,
67 # when combined with configuration changes causing disk scans. That
68 # would also permit using a completely stubbed out file system,
69 # reducing IO in tests that use UniqueFileAllocator (such as all the
70 # pagetests in Launchpad. At that point an interface to obtain a
71 # factory of UniqueFileAllocator's would be useful to parameterise the
72 # entire test suite.
73 namer = UniqueFileAllocator(self._tempdir, 'OOPS', 'T')
74 # first name of the day
75 self.assertUniqueFileAllocator(namer,
76 datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC),
77 'OOPS-91T1', 1, '2006-04-01/01800.T1', '2006-04-01')
78 # second name of the day
79 self.assertUniqueFileAllocator(namer,
80 datetime.datetime(2006, 04, 01, 12, 00, 00, tzinfo=UTC),
81 'OOPS-91T2', 2, '2006-04-01/43200.T2', '2006-04-01')
82
83 # first name of the following day sets a new dir and the id starts
84 # over.
85 self.assertUniqueFileAllocator(namer,
86 datetime.datetime(2006, 04, 02, 00, 30, 00, tzinfo=UTC),
87 'OOPS-92T1', 1, '2006-04-02/01800.T1', '2006-04-02')
88
89 # Setting a token inserts the token into the filename.
90 namer.setToken('YYY')
91 logid, filename = namer.newId(
92 datetime.datetime(2006, 04, 02, 00, 30, 00, tzinfo=UTC))
93 self.assertEqual(logid, 'OOPS-92TYYY2')
94
95 # Setting a type controls the log id:
96 namer.setToken('')
97 namer._log_type = "PROFILE"
98 logid, filename = namer.newId(
99 datetime.datetime(2006, 04, 02, 00, 30, 00, tzinfo=UTC))
100 self.assertEqual(logid, 'PROFILE-92T3')
101
102 # Native timestamps are not permitted - UTC only.
103 now = datetime.datetime(2006, 04, 02, 00, 30, 00)
104 self.assertRaises(ValueError, namer.newId, now)
105
106 def test_changeErrorDir(self):
107 """Test changing the log output dir."""
108 namer = UniqueFileAllocator(self._tempdir, 'OOPS', 'T')
109
110 # First an id in the original error directory.
111 self.assertUniqueFileAllocator(namer,
112 datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC),
113 'OOPS-91T1', 1, '2006-04-01/01800.T1', '2006-04-01')
114
115 # UniqueFileAllocator uses the _output_root attribute to get the
116 # current output directory.
117 new_output_dir = self.useFixture(TempDir()).path
118 namer._output_root = new_output_dir
119
120 # Now an id on the same day, in the new directory.
121 now = datetime.datetime(2006, 04, 01, 12, 00, 00, tzinfo=UTC)
122 log_id, filename = namer.newId(now)
123
124 # Since it's a new directory, with no previous logs, the id is 1
125 # again, rather than 2.
126 self.assertEqual(log_id, 'OOPS-91T1')
127 self.assertEqual(namer._last_serial, 1)
128 self.assertEqual(namer._last_output_dir,
129 os.path.join(new_output_dir, '2006-04-01'))
130
131 def test_findHighestSerial(self):
132 namer = UniqueFileAllocator(self._tempdir, "OOPS", "T")
133 # Creates the dir using now as the timestamp.
134 output_dir = namer.output_dir()
135 # write some files, in non-serial order.
136 open(os.path.join(output_dir, '12343.T1'), 'w').close()
137 open(os.path.join(output_dir, '12342.T2'), 'w').close()
138 open(os.path.join(output_dir, '12345.T3'), 'w').close()
139 open(os.path.join(output_dir, '1234567.T0010'), 'w').close()
140 open(os.path.join(output_dir, '12346.A42'), 'w').close()
141 open(os.path.join(output_dir, '12346.B100'), 'w').close()
142 # The namer should figure out the right highest serial.
143 self.assertEqual(namer._findHighestSerial(output_dir), 10)
144
145 def test_output_dir_permission(self):
146 # Set up default dir creation mode to rwx------.
147 umask_permission = stat.S_IRWXG | stat.S_IRWXO
148 old_umask = os.umask(umask_permission)
149 namer = UniqueFileAllocator(self._tempdir, "OOPS", "T")
150 output_dir = namer.output_dir()
151 st = os.stat(output_dir)
152 # Permission we want here is: rwxr-xr-x
153 wanted_permission = (
154 stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH |
155 stat.S_IXOTH)
156 # Get only the permission bits for this directory.
157 dir_permission = stat.S_IMODE(st.st_mode)
158 self.assertEqual(dir_permission, wanted_permission)
159 # Restore the umask to the original value.
160 ignored = os.umask(old_umask)
1610
=== removed file 'oops_datedir_repo/uniquefileallocator.py'
--- oops_datedir_repo/uniquefileallocator.py 2011-11-11 04:21:05 +0000
+++ oops_datedir_repo/uniquefileallocator.py 1970-01-01 00:00:00 +0000
@@ -1,212 +0,0 @@
1# Copyright (c) 2010, 2011, Canonical Ltd
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published by
5# the Free Software Foundation, version 3 only.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU Lesser General Public License for more details.
11#
12# You should have received a copy of the GNU Lesser General Public License
13# along with this program. If not, see <http://www.gnu.org/licenses/>.
14# GNU Lesser General Public License version 3 (see the file LICENSE).
15
16
17"""Create uniquely named log files on disk."""
18
19
20__all__ = ['UniqueFileAllocator']
21
22__metaclass__ = type
23
24
25import datetime
26import errno
27import os.path
28import stat
29import threading
30
31import pytz
32
33
34UTC = pytz.utc
35
36# the section of the ID before the instance identifier is the
37# days since the epoch, which is defined as the start of 2006.
38epoch = datetime.datetime(2006, 01, 01, 00, 00, 00, tzinfo=UTC)
39
40
41class UniqueFileAllocator:
42 """Assign unique file names to logs being written from an app/script.
43
44 UniqueFileAllocator causes logs written from one process to be uniquely
45 named. It is not safe for use in multiple processes with the same output
46 root - each process must have a unique output root.
47 """
48
49 def __init__(self, output_root, log_type, log_subtype):
50 """Create a UniqueFileAllocator.
51
52 :param output_root: The root directory that logs should be placed in.
53 :param log_type: A string to use as a prefix in the ID assigned to new
54 logs. For instance, "OOPS".
55 :param log_subtype: A string to insert in the generate log filenames
56 between the day number and the serial. For instance "T" for
57 "Testing".
58 """
59 self._lock = threading.Lock()
60 self._output_root = output_root
61 self._last_serial = 0
62 self._last_output_dir = None
63 self._log_type = log_type
64 self._log_subtype = log_subtype
65 self._log_token = ""
66
67 def _findHighestSerialFilename(self, directory=None, time=None):
68 """Find details of the last log present in the given directory.
69
70 This function only considers logs with the currently
71 configured log_subtype.
72
73 One of directory, time must be supplied.
74
75 :param directory: Look in this directory.
76 :param time: Look in the directory that a log written at this time
77 would have been written to. If supplied, supercedes directory.
78 :return: a tuple (log_serial, log_filename), which will be (0,
79 None) if no logs are found. log_filename is a usable path, not
80 simply the basename.
81 """
82 if directory is None:
83 directory = self.output_dir(time)
84 prefix = self.get_log_infix()
85 lastid = 0
86 lastfilename = None
87 for filename in os.listdir(directory):
88 logid = filename.rsplit('.', 1)[1]
89 if not logid.startswith(prefix):
90 continue
91 logid = logid[len(prefix):]
92 if logid.isdigit() and (lastid is None or int(logid) > lastid):
93 lastid = int(logid)
94 lastfilename = filename
95 if lastfilename is not None:
96 lastfilename = os.path.join(directory, lastfilename)
97 return lastid, lastfilename
98
99 def _findHighestSerial(self, directory):
100 """Find the last serial actually applied to disk in directory.
101
102 The purpose of this function is to not repeat sequence numbers
103 if the logging application is restarted.
104
105 This method is not thread safe, and only intended to be called
106 from the constructor (but it is called from other places in
107 integration tests).
108 """
109 return self._findHighestSerialFilename(directory)[0]
110
111 def getFilename(self, log_serial, time):
112 """Get the filename for a given log serial and time."""
113 log_subtype = self.get_log_infix()
114 # TODO: Calling output_dir causes a global lock to be taken and a
115 # directory scan, which is bad for performance. It would be better
116 # to have a split out 'directory name for time' function which the
117 # 'want to use this directory now' function can call.
118 output_dir = self.output_dir(time)
119 second_in_day = time.hour * 3600 + time.minute * 60 + time.second
120 return os.path.join(
121 output_dir, '%05d.%s%s' % (
122 second_in_day, log_subtype, log_serial))
123
124 def get_log_infix(self):
125 """Return the current log infix to use in ids and file names."""
126 return self._log_subtype + self._log_token
127
128 def newId(self, now=None):
129 """Returns an (id, filename) pair for use by the caller.
130
131 The ID is composed of a short string to identify the Launchpad
132 instance followed by an ID that is unique for the day.
133
134 The filename is composed of the zero padded second in the day
135 followed by the ID. This ensures that reports are in date order when
136 sorted lexically.
137 """
138 if now is not None:
139 now = now.astimezone(UTC)
140 else:
141 now = datetime.datetime.now(UTC)
142 # We look up the error directory before allocating a new ID,
143 # because if the day has changed, errordir() will reset the ID
144 # counter to zero.
145 self.output_dir(now)
146 self._lock.acquire()
147 try:
148 self._last_serial += 1
149 newid = self._last_serial
150 finally:
151 self._lock.release()
152 subtype = self.get_log_infix()
153 day_number = (now - epoch).days + 1
154 log_id = '%s-%d%s%d' % (self._log_type, day_number, subtype, newid)
155 filename = self.getFilename(newid, now)
156 return log_id, filename
157
158 def output_dir(self, now=None):
159 """Find or make the directory to allocate log names in.
160
161 Log names are assigned within subdirectories containing the date the
162 assignment happened.
163 """
164 if now is not None:
165 now = now.astimezone(UTC)
166 else:
167 now = datetime.datetime.now(UTC)
168 date = now.strftime('%Y-%m-%d')
169 result = os.path.join(self._output_root, date)
170 if result != self._last_output_dir:
171 self._lock.acquire()
172 try:
173 self._last_output_dir = result
174 # make sure the directory exists
175 try:
176 os.makedirs(result)
177 except OSError, e:
178 if e.errno != errno.EEXIST:
179 raise
180 # Make sure the directory permission is set to: rwxr-xr-x
181 permission = (
182 stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
183 stat.S_IROTH | stat.S_IXOTH)
184 os.chmod(result, permission)
185 # TODO: Note that only one process can do this safely: its not
186 # cross-process safe, and also not entirely threadsafe:
187 # another # thread that has a new log and hasn't written it
188 # could then use that serial number. We should either make it
189 # really safe, or remove the contention entirely and log
190 # uniquely per thread of execution.
191 self._last_serial = self._findHighestSerial(result)
192 finally:
193 self._lock.release()
194 return result
195
196 def listRecentReportFiles(self):
197 now = datetime.datetime.now(UTC)
198 yesterday = now - datetime.timedelta(days=1)
199 directories = [self.output_dir(now), self.output_dir(yesterday)]
200 for directory in directories:
201 report_names = os.listdir(directory)
202 for name in sorted(report_names, reverse=True):
203 yield directory, name
204
205 def setToken(self, token):
206 """Append a string to the log subtype in filenames and log ids.
207
208 :param token: a string to append..
209 Scripts that run multiple processes can use this to create a
210 unique identifier for each process.
211 """
212 self._log_token = token

Subscribers

People subscribed via source and target branches

to all changes: