Merge lp:~jameinel/bzr/2.0.4-autopack-rename-507557 into lp:bzr/2.0

Proposed by John A Meinel
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jameinel/bzr/2.0.4-autopack-rename-507557
Merge into: lp:bzr/2.0
Diff against target: 358 lines (+166/-26)
4 files modified
NEWS (+7/-0)
bzrlib/repofmt/groupcompress_repo.py (+6/-5)
bzrlib/repofmt/pack_repo.py (+57/-19)
bzrlib/tests/test_repository.py (+96/-2)
To merge this branch: bzr merge lp:~jameinel/bzr/2.0.4-autopack-rename-507557
Reviewer Review Type Date Requested Status
Vincent Ladeuil Approve
Robert Collins (community) based on your description alone. Approve
Review via email: mp+17778@code.launchpad.net
To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

I'm pretty happy with the result, but I'm not 100% sure this is a small enough change to go into 2.0.*

Anyway, this changes a few bits of the autopacking code.

1) When renaming a file into 'obsolete_packs', if we get an error, just continue on. We will generate a warning, but it shouldn't interrupt the process (it was already removed from the list of referenced files.)

2) When deleting files from 'obsolete_packs' don't delete files that we currently have scheduled for obsoletion. "If I'm scheduling it 'right now' then probably so did someone else, and they beat me to it."

3) For files found in (2) don't try to rename them (since we know they are already in obsolete_packs).

4) Go one step further. If a file we are about to obsolete has already been removed from 'pack-names', then we can assume that someone else has already scheduled its obsoletion and will be doing the rename instead of us.

This should fix most of the problems in bug #507557. I don't know if it also solves some of the other autopack concurrency issues, but it should at least help with that one.

Revision history for this message
Robert Collins (lifeless) wrote :

 review: +1 based on your description alone.

-Rob

review: Approve (based on your description alone.)
Revision history for this message
Vincent Ladeuil (vila) wrote :

The fix is clearly demonstrated by the associated tests which makes
it a good candidate even for 2.0.5.

We may want some staging period so I think release *candidate* is good for that.

review: Approve
Revision history for this message
Gareth White (gwhite-deactivatedaccount) wrote :

(This isn't a review, just some comments from an interested third party...)
* I've done some user-level testing with your fixes and they do appear to resolve the original bug. When stress-testing Bazaar my commits now all succeed, aside from the occasional instance of bug 507553 ;)

* I would suggest making the warnings into mutters simply because (a) they're not something the user should be alarmed about, and (b) when you get them it can be quite spammy (e.g. 6 files per revision = 54 warnings in the worst case). However, see next point.

* I noticed that groupcompress_repo.py already contains a copy of _execute_pack_operations() so you'll probably want to apply your changes there as well (or do some refactoring to eliminate the copy - it's only a couple of lines different). I think this is why I was seeing so many warnings when running my stress test with a 2a repo - it wasn't actually exercising most of your new code.

* I manually applied the above changes and repeated the stress test, and didn't get a warning at all. However, it did appear to be leaving around some pack files in the "packs" directory. I suspect this is because the single-revision pack that's created during a commit just before an autopack never gets added to pack-names. So the "if o.name not in already_obsolete and o.name in orig_disk_names" test fails.

* You've probably already fixed it locally, but I noticed there's still some debugging code in reload_pack_names() ;)

Revision history for this message
Gareth White (gwhite-deactivatedaccount) wrote :

As a customer it would be nice to have this in 2.0.x but since 2.1 is so close I wouldn't be so worried about it.

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Gareth White wrote:
> (This isn't a review, just some comments from an interested third party...)
> * I've done some user-level testing with your fixes and they do appear to resolve the original bug. When stress-testing Bazaar my commits now all succeed, aside from the occasional instance of bug 507553 ;)
>
> * I would suggest making the warnings into mutters simply because (a) they're not something the user should be alarmed about, and (b) when you get them it can be quite spammy (e.g. 6 files per revision = 54 warnings in the worst case). However, see next point.
>
> * I noticed that groupcompress_repo.py already contains a copy of _execute_pack_operations() so you'll probably want to apply your changes there as well (or do some refactoring to eliminate the copy - it's only a couple of lines different). I think this is why I was seeing so many warnings when running my stress test with a 2a repo - it wasn't actually exercising most of your new code.

Nice catch.

>
> * I manually applied the above changes and repeated the stress test, and didn't get a warning at all. However, it did appear to be leaving around some pack files in the "packs" directory. I suspect this is because the single-revision pack that's created during a commit just before an autopack never gets added to pack-names. So the "if o.name not in already_obsolete and o.name in orig_disk_names" test fails.

I'm a little concerned about this one. Partially because my direct
testing didn't show this behavior. Leaving around an extra pack file on
every autopack would be bad. Or does this only happen if there is
contention?

>
> * You've probably already fixed it locally, but I noticed there's still some debugging code in reload_pack_names() ;)

Removed. I'm surprised Vila didn't catch it, he's usually pretty good
about spotting my 'pdb' lines. :)

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAktYiUIACgkQJdeBCYSNAAMuGgCeOh/C7TX9JYnBA+i8hPdteGnw
3iMAoNn6O9sZu77LlxEDqfHfw6dOuGD1
=haMF
-----END PGP SIGNATURE-----

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

>> * I manually applied the above changes and repeated the stress test, and didn't get a warning at all. However, it did appear to be leaving around some pack files in the "packs" directory. I suspect this is because the single-revision pack that's created during a commit just before an autopack never gets added to pack-names. So the "if o.name not in already_obsolete and o.name in orig_disk_names" test fails.
>
> I'm a little concerned about this one. Partially because my direct
> testing didn't show this behavior. Leaving around an extra pack file on
> every autopack would be bad. Or does this only happen if there is
> contention?
>

I've confirmed this (with tests that fail), and I'm in the process of
adding a localized test for this behavior. There have been enough rough
spots that I'll probably just land this for 2.1.0rc1 to start with, and
then keep the patch around for future 2.0.5 once we've had some time
with it.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAktYlaMACgkQJdeBCYSNAAMsoQCcDUvjqgpiCPgLaJnzRSvH42zj
7JYAoLuasGQV3kleC5jAT4QhpscOFU64
=PrPm
-----END PGP SIGNATURE-----

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

John A Meinel wrote:
>
>>> * I manually applied the above changes and repeated the stress test, and didn't get a warning at all. However, it did appear to be leaving around some pack files in the "packs" directory. I suspect this is because the single-revision pack that's created during a commit just before an autopack never gets added to pack-names. So the "if o.name not in already_obsolete and o.name in orig_disk_names" test fails.
>> I'm a little concerned about this one. Partially because my direct
>> testing didn't show this behavior. Leaving around an extra pack file on
>> every autopack would be bad. Or does this only happen if there is
>> contention?
>
>
> I've confirmed this (with tests that fail), and I'm in the process of
> adding a localized test for this behavior. There have been enough rough
> spots that I'll probably just land this for 2.1.0rc1 to start with, and
> then keep the patch around for future 2.0.5 once we've had some time
> with it.
>
> John
> =:->

Here is a possible fix. I don't really like the "isinstance()" check,
but it does fit. (If a 'pack' is a new then it isn't going to be in the
pack-names file, etc.)

The only other problem is that I think a pack stays "NewPack" between
multiple calls to .commit_write_group(), rather than being converted to
ExistingPack after commit_write_group finishes. (It could be done in
synchronize_pack_names_from_disk_nodes, I think.)

I'm tempted to just get rid of that check, and go ahead and let both
clients try to rename all obsolete packs (except for ones it has already
found in the target). And then downgrade the 'warning' to 'mutter'.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAktYqE8ACgkQJdeBCYSNAAMEfACfX3qLSvUK6XS880U8b12NUUr5
PnoAoIRfPd52mUOK3mHB3id4FH8MoFim
=opCG
-----END PGP SIGNATURE-----

Revision history for this message
Robert Collins (lifeless) wrote :

On Thu, 2010-01-21 at 19:18 +0000, John A Meinel wrote:
>
>
> I'm tempted to just get rid of that check, and go ahead and let both
> clients try to rename all obsolete packs (except for ones it has
> already
> found in the target). And then downgrade the 'warning' to 'mutter'.

+1 on this.

-Rob

Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

Is this fully approved now (given Rob's vote) or do you need a review still?

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Ian Clatworthy wrote:
> Is this fully approved now (given Rob's vote) or do you need a review still?

I think this is fully approved (but not marked as such?) I landed it in
2.1 rather than trying to squeeze it into the 2.0.4 series. (There was a
fair number of bumps to this, and I didn't want to push it just before
release.)

It has gotten a bit of a back burner, but I'm not sure right now whether
I want to push for 2.0.5, or just forget it and just have it be in 2.1.

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAktio84ACgkQJdeBCYSNAAPC9gCfRY+62xkMrE8ryweD/5noc74G
VPEAn2miqkg2rmgok8AsCr3H3b6yEK7p
=Y0r5
-----END PGP SIGNATURE-----

Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

> It has gotten a bit of a back burner, but I'm not sure right now whether
> I want to push for 2.0.5, or just forget it and just have it be in 2.1.

The linked bug has Low priority (fwiw) and Gareth seems ok with it being in 2.1 only. At this point, I'd be tempted to leave it out of the 2.0 series.

Revision history for this message
Vincent Ladeuil (vila) wrote :

John, did you land this patch into 2.1 ? 2.0 ? Dev ?

I'd be inclined to land it into 2.1 only (and to dev from there)
but since you're the RM, it's up to you.

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Vincent Ladeuil wrote:
> John, did you land this patch into 2.1 ? 2.0 ? Dev ?
>
> I'd be inclined to land it into 2.1 only (and to dev from there)
> but since you're the RM, it's up to you.

It has landed in 2.1 (a while ago), but since it was originally proposed
for 2.0 it stays around until we land it there. We could mark it as
merged if you find it is in your way, though since we are considering it
for a 2.0.5 inclusion, I didn't want it completely gone.

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAktxkxsACgkQJdeBCYSNAAOosgCfVPbXNwrkGj2Aocr+HZd8Xnck
d1sAoJ3TSLD24X3SMCHXkksA/SPM87bu
=xXYZ
-----END PGP SIGNATURE-----

Revision history for this message
Martin Pool (mbp) wrote :

John, I think you should either land this, or supersede it if we actually don't care about putting it into 2.0. Given as Ian says the bug is assessed as low and the patch is not trivial, I would probably lean towards not landing it, but either is fine.

I don't want us getting to tolerate a long review queue because other stuff will get lost.

Revision history for this message
Vincent Ladeuil (vila) wrote :

Marking merged since 2.1 is out.
I think we should focus on pushing 2.1 everywhere instead of doing a 2.0.5.

Revision history for this message
Robert Collins (lifeless) wrote :

On Mon, 2010-03-01 at 08:04 +0000, Vincent Ladeuil wrote:
> Marking merged since 2.1 is out.
> I think we should focus on pushing 2.1 everywhere instead of doing a 2.0.5.

We're very unlikely to get 2.1 into karmic/hardy etc. Just saying :)

-Rob

Revision history for this message
Martin Pool (mbp) wrote :

On 1 March 2010 19:24, Robert Collins <email address hidden> wrote:
> On Mon, 2010-03-01 at 08:04 +0000, Vincent Ladeuil wrote:
>> Marking merged since 2.1 is out.
>> I think we should focus on pushing 2.1 everywhere instead of doing a 2.0.5.
>
> We're very unlikely to get 2.1 into karmic/hardy etc. Just saying :)

Yes, 2.0 is still very much alive, and should get all patches that
make sense to fix there. However, if the bug is not severe or the fix
is not trivial, it's ok to leave it out.

Generally if the same patch will apply to both 2.1 and 2.0 it should
be merged to 2.0. 2.0 doesn't have a higher bar than 2.1.

--
Martin <http://launchpad.net/~mbp/>

Revision history for this message
Vincent Ladeuil (vila) wrote :

Yeah, digging while you were commenting :) I realized we already have quite a bit of mess between the various patches already landed in the 2.0 and 2.1 branches, I'm cleaning up and will land this patch in the 2.0 branch before merging into 2.1 and then into bzr.dev.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2010-01-17 11:11:34 +0000
3+++ NEWS 2010-01-21 19:29:09 +0000
4@@ -38,6 +38,13 @@
5 returns ``EINTR`` by calling ``PyErr_CheckSignals``. This affected the
6 optional ``_readdir_pyx`` extension. (Andrew Bennetts, #495023)
7
8+* Concurrent autopacking is more resilient to already-renamed pack files.
9+ If we find that a file we are about to obsolete is already obsoleted, we
10+ do not try to rename it, and we leave the file in ``obsolete_packs``.
11+ The code is also fault tolerant if a file goes missing, assuming that
12+ another process already removed the file.
13+ (John Arbash Meinel, Gareth White, #507557)
14+
15 * Concurrent autopacks will no longer lose a newly created pack file.
16 There was a race condition, where if the reload happened at the right
17 time, the second packer would forget the name of the newly added pack
18
19=== modified file 'bzrlib/repofmt/groupcompress_repo.py'
20--- bzrlib/repofmt/groupcompress_repo.py 2009-12-16 20:20:04 +0000
21+++ bzrlib/repofmt/groupcompress_repo.py 2010-01-21 19:29:09 +0000
22@@ -1,4 +1,4 @@
23-# Copyright (C) 2008, 2009 Canonical Ltd
24+# Copyright (C) 2008, 2009, 2010 Canonical Ltd
25 #
26 # This program is free software; you can redistribute it and/or modify
27 # it under the terms of the GNU General Public License as published by
28@@ -704,10 +704,11 @@
29 self._remove_pack_from_memory(pack)
30 # record the newly available packs and stop advertising the old
31 # packs
32- result = self._save_pack_names(clear_obsolete_packs=True)
33- # Move the old packs out of the way now they are no longer referenced.
34- for revision_count, packs in pack_operations:
35- self._obsolete_packs(packs)
36+ to_be_obsoleted = []
37+ for _, packs in pack_operations:
38+ to_be_obsoleted.extend(packs)
39+ result = self._save_pack_names(clear_obsolete_packs=True,
40+ obsolete_packs=to_be_obsoleted)
41 return result
42
43
44
45=== modified file 'bzrlib/repofmt/pack_repo.py'
46--- bzrlib/repofmt/pack_repo.py 2010-01-16 20:46:03 +0000
47+++ bzrlib/repofmt/pack_repo.py 2010-01-21 19:29:09 +0000
48@@ -1541,10 +1541,11 @@
49 self._remove_pack_from_memory(pack)
50 # record the newly available packs and stop advertising the old
51 # packs
52- result = self._save_pack_names(clear_obsolete_packs=True)
53- # Move the old packs out of the way now they are no longer referenced.
54- for revision_count, packs in pack_operations:
55- self._obsolete_packs(packs)
56+ to_be_obsoleted = []
57+ for _, packs in pack_operations:
58+ to_be_obsoleted.extend(packs)
59+ result = self._save_pack_names(clear_obsolete_packs=True,
60+ obsolete_packs=to_be_obsoleted)
61 return result
62
63 def _flush_new_pack(self):
64@@ -1784,8 +1785,13 @@
65 :param return: None.
66 """
67 for pack in packs:
68- pack.pack_transport.rename(pack.file_name(),
69- '../obsolete_packs/' + pack.file_name())
70+ try:
71+ pack.pack_transport.rename(pack.file_name(),
72+ '../obsolete_packs/' + pack.file_name())
73+ except (errors.PathError, errors.TransportError), e:
74+ # TODO: Should these be warnings or mutters?
75+ mutter("couldn't rename obsolete pack, skipping it:\n%s"
76+ % (e,))
77 # TODO: Probably needs to know all possible indices for this pack
78 # - or maybe list the directory and move all indices matching this
79 # name whether we recognize it or not?
80@@ -1793,8 +1799,12 @@
81 if self.chk_index is not None:
82 suffixes.append('.cix')
83 for suffix in suffixes:
84- self._index_transport.rename(pack.name + suffix,
85- '../obsolete_packs/' + pack.name + suffix)
86+ try:
87+ self._index_transport.rename(pack.name + suffix,
88+ '../obsolete_packs/' + pack.name + suffix)
89+ except (errors.PathError, errors.TransportError), e:
90+ mutter("couldn't rename obsolete index, skipping it:\n%s"
91+ % (e,))
92
93 def pack_distribution(self, total_revisions):
94 """Generate a list of the number of revisions to put in each pack.
95@@ -1872,6 +1882,7 @@
96 disk_nodes = set()
97 for index, key, value in self._iter_disk_pack_index():
98 disk_nodes.add((key, value))
99+ orig_disk_nodes = set(disk_nodes)
100
101 # do a two-way diff against our original content
102 current_nodes = set()
103@@ -1890,7 +1901,7 @@
104 disk_nodes.difference_update(deleted_nodes)
105 disk_nodes.update(new_nodes)
106
107- return disk_nodes, deleted_nodes, new_nodes
108+ return disk_nodes, deleted_nodes, new_nodes, orig_disk_nodes
109
110 def _syncronize_pack_names_from_disk_nodes(self, disk_nodes):
111 """Given the correct set of pack files, update our saved info.
112@@ -1936,7 +1947,7 @@
113 added.append(name)
114 return removed, added, modified
115
116- def _save_pack_names(self, clear_obsolete_packs=False):
117+ def _save_pack_names(self, clear_obsolete_packs=False, obsolete_packs=None):
118 """Save the list of packs.
119
120 This will take out the mutex around the pack names list for the
121@@ -1946,12 +1957,16 @@
122
123 :param clear_obsolete_packs: If True, clear out the contents of the
124 obsolete_packs directory.
125+ :param obsolete_packs: Packs that are obsolete once the new pack-names
126+ file has been written.
127 :return: A list of the names saved that were not previously on disk.
128 """
129+ already_obsolete = []
130 self.lock_names()
131 try:
132 builder = self._index_builder_class()
133- disk_nodes, deleted_nodes, new_nodes = self._diff_pack_names()
134+ (disk_nodes, deleted_nodes, new_nodes,
135+ orig_disk_nodes) = self._diff_pack_names()
136 # TODO: handle same-name, index-size-changes here -
137 # e.g. use the value from disk, not ours, *unless* we're the one
138 # changing it.
139@@ -1959,14 +1974,25 @@
140 builder.add_node(key, value)
141 self.transport.put_file('pack-names', builder.finish(),
142 mode=self.repo.bzrdir._get_file_mode())
143- # move the baseline forward
144 self._packs_at_load = disk_nodes
145 if clear_obsolete_packs:
146- self._clear_obsolete_packs()
147+ to_preserve = None
148+ if obsolete_packs:
149+ to_preserve = set([o.name for o in obsolete_packs])
150+ already_obsolete = self._clear_obsolete_packs(to_preserve)
151 finally:
152 self._unlock_names()
153 # synchronise the memory packs list with what we just wrote:
154 self._syncronize_pack_names_from_disk_nodes(disk_nodes)
155+ if obsolete_packs:
156+ # TODO: We could add one more condition here. "if o.name not in
157+ # orig_disk_nodes and o != the new_pack we haven't written to
158+ # disk yet. However, the new pack object is not easily
159+ # accessible here (it would have to be passed through the
160+ # autopacking code, etc.)
161+ obsolete_packs = [o for o in obsolete_packs
162+ if o.name not in already_obsolete]
163+ self._obsolete_packs(obsolete_packs)
164 return [new_node[0][0] for new_node in new_nodes]
165
166 def reload_pack_names(self):
167@@ -1987,13 +2013,12 @@
168 if first_read:
169 return True
170 # out the new value.
171- disk_nodes, deleted_nodes, new_nodes = self._diff_pack_names()
172+ (disk_nodes, deleted_nodes, new_nodes,
173+ orig_disk_nodes) = self._diff_pack_names()
174 # _packs_at_load is meant to be the explicit list of names in
175 # 'pack-names' at then start. As such, it should not contain any
176 # pending names that haven't been written out yet.
177- pack_names_nodes = disk_nodes.difference(new_nodes)
178- pack_names_nodes.update(deleted_nodes)
179- self._packs_at_load = pack_names_nodes
180+ self._packs_at_load = orig_disk_nodes
181 (removed, added,
182 modified) = self._syncronize_pack_names_from_disk_nodes(disk_nodes)
183 if removed or added or modified:
184@@ -2008,15 +2033,28 @@
185 raise
186 raise errors.RetryAutopack(self.repo, False, sys.exc_info())
187
188- def _clear_obsolete_packs(self):
189+ def _clear_obsolete_packs(self, preserve=None):
190 """Delete everything from the obsolete-packs directory.
191+
192+ :return: A list of pack identifiers (the filename without '.pack') that
193+ were found in obsolete_packs.
194 """
195+ found = []
196 obsolete_pack_transport = self.transport.clone('obsolete_packs')
197+ if preserve is None:
198+ preserve = set()
199 for filename in obsolete_pack_transport.list_dir('.'):
200+ name, ext = osutils.splitext(filename)
201+ if ext == '.pack':
202+ found.append(name)
203+ if name in preserve:
204+ continue
205 try:
206 obsolete_pack_transport.delete(filename)
207 except (errors.PathError, errors.TransportError), e:
208- warning("couldn't delete obsolete pack, skipping it:\n%s" % (e,))
209+ warning("couldn't delete obsolete pack, skipping it:\n%s"
210+ % (e,))
211+ return found
212
213 def _start_write_group(self):
214 # Do not permit preparation for writing if we're not in a 'write lock'.
215
216=== modified file 'bzrlib/tests/test_repository.py'
217--- bzrlib/tests/test_repository.py 2010-01-16 20:46:03 +0000
218+++ bzrlib/tests/test_repository.py 2010-01-21 19:29:09 +0000
219@@ -1082,6 +1082,31 @@
220 packs.ensure_loaded()
221 return tree, r, packs, [rev1, rev2, rev3]
222
223+ def test__clear_obsolete_packs(self):
224+ packs = self.get_packs()
225+ obsolete_pack_trans = packs.transport.clone('obsolete_packs')
226+ obsolete_pack_trans.put_bytes('a-pack.pack', 'content\n')
227+ obsolete_pack_trans.put_bytes('a-pack.rix', 'content\n')
228+ obsolete_pack_trans.put_bytes('a-pack.iix', 'content\n')
229+ obsolete_pack_trans.put_bytes('another-pack.pack', 'foo\n')
230+ obsolete_pack_trans.put_bytes('not-a-pack.rix', 'foo\n')
231+ res = packs._clear_obsolete_packs()
232+ self.assertEqual(['a-pack', 'another-pack'], sorted(res))
233+ self.assertEqual([], obsolete_pack_trans.list_dir('.'))
234+
235+ def test__clear_obsolete_packs_preserve(self):
236+ packs = self.get_packs()
237+ obsolete_pack_trans = packs.transport.clone('obsolete_packs')
238+ obsolete_pack_trans.put_bytes('a-pack.pack', 'content\n')
239+ obsolete_pack_trans.put_bytes('a-pack.rix', 'content\n')
240+ obsolete_pack_trans.put_bytes('a-pack.iix', 'content\n')
241+ obsolete_pack_trans.put_bytes('another-pack.pack', 'foo\n')
242+ obsolete_pack_trans.put_bytes('not-a-pack.rix', 'foo\n')
243+ res = packs._clear_obsolete_packs(preserve=set(['a-pack']))
244+ self.assertEqual(['a-pack', 'another-pack'], sorted(res))
245+ self.assertEqual(['a-pack.iix', 'a-pack.pack', 'a-pack.rix'],
246+ sorted(obsolete_pack_trans.list_dir('.')))
247+
248 def test__max_pack_count(self):
249 """The maximum pack count is a function of the number of revisions."""
250 # no revisions - one pack, so that we can have a revision free repo
251@@ -1107,6 +1132,28 @@
252 # check some arbitrary big numbers
253 self.assertEqual(25, packs._max_pack_count(112894))
254
255+ def test__obsolete_packs(self):
256+ tree, r, packs, revs = self.make_packs_and_alt_repo(write_lock=True)
257+ names = packs.names()
258+ pack = packs.get_pack_by_name(names[0])
259+ # Schedule this one for removal
260+ packs._remove_pack_from_memory(pack)
261+ # Simulate a concurrent update by renaming the .pack file and one of
262+ # the indices
263+ packs.transport.rename('packs/%s.pack' % (names[0],),
264+ 'obsolete_packs/%s.pack' % (names[0],))
265+ packs.transport.rename('indices/%s.iix' % (names[0],),
266+ 'obsolete_packs/%s.iix' % (names[0],))
267+ # Now trigger the obsoletion, and ensure that all the remaining files
268+ # are still renamed
269+ packs._obsolete_packs([pack])
270+ self.assertEqual([n + '.pack' for n in names[1:]],
271+ sorted(packs._pack_transport.list_dir('.')))
272+ # names[0] should not be present in the index anymore
273+ self.assertEqual(names[1:],
274+ sorted(set([osutils.splitext(n)[0] for n in
275+ packs._index_transport.list_dir('.')])))
276+
277 def test_pack_distribution_zero(self):
278 packs = self.get_packs()
279 self.assertEqual([0], packs.pack_distribution(0))
280@@ -1300,7 +1347,7 @@
281 removed_pack = packs.get_pack_by_name(to_remove_name)
282 packs._remove_pack_from_memory(removed_pack)
283 names = packs.names()
284- all_nodes, deleted_nodes, new_nodes = packs._diff_pack_names()
285+ all_nodes, deleted_nodes, new_nodes, _ = packs._diff_pack_names()
286 new_names = set([x[0][0] for x in new_nodes])
287 self.assertEqual(names, sorted([x[0][0] for x in all_nodes]))
288 self.assertEqual(set(names) - set(orig_names), new_names)
289@@ -1311,7 +1358,7 @@
290 reloaded_names = packs.names()
291 self.assertEqual(orig_at_load, packs._packs_at_load)
292 self.assertEqual(names, reloaded_names)
293- all_nodes, deleted_nodes, new_nodes = packs._diff_pack_names()
294+ all_nodes, deleted_nodes, new_nodes, _ = packs._diff_pack_names()
295 new_names = set([x[0][0] for x in new_nodes])
296 self.assertEqual(names, sorted([x[0][0] for x in all_nodes]))
297 self.assertEqual(set(names) - set(orig_names), new_names)
298@@ -1319,6 +1366,21 @@
299 self.assertEqual([to_remove_name],
300 sorted([x[0][0] for x in deleted_nodes]))
301
302+ def test_autopack_obsoletes_new_pack(self):
303+ tree, r, packs, revs = self.make_packs_and_alt_repo(write_lock=True)
304+ packs._max_pack_count = lambda x: 1
305+ packs.pack_distribution = lambda x: [10]
306+ r.start_write_group()
307+ r.revisions.insert_record_stream([versionedfile.FulltextContentFactory(
308+ ('bogus-rev',), (), None, 'bogus-content\n')])
309+ # This should trigger an autopack, which will combine everything into a
310+ # single pack file.
311+ new_names = r.commit_write_group()
312+ names = packs.names()
313+ self.assertEqual(1, len(names))
314+ self.assertEqual([names[0] + '.pack'],
315+ packs._pack_transport.list_dir('.'))
316+
317 def test_autopack_reloads_and_stops(self):
318 tree, r, packs, revs = self.make_packs_and_alt_repo(write_lock=True)
319 # After we have determined what needs to be autopacked, trigger a
320@@ -1336,6 +1398,38 @@
321 self.assertEqual(tree.branch.repository._pack_collection.names(),
322 packs.names())
323
324+ def test__save_pack_names(self):
325+ tree, r, packs, revs = self.make_packs_and_alt_repo(write_lock=True)
326+ names = packs.names()
327+ pack = packs.get_pack_by_name(names[0])
328+ packs._remove_pack_from_memory(pack)
329+ packs._save_pack_names(obsolete_packs=[pack])
330+ cur_packs = packs._pack_transport.list_dir('.')
331+ self.assertEqual([n + '.pack' for n in names[1:]], sorted(cur_packs))
332+ # obsolete_packs will also have stuff like .rix and .iix present.
333+ obsolete_packs = packs.transport.list_dir('obsolete_packs')
334+ obsolete_names = set([osutils.splitext(n)[0] for n in obsolete_packs])
335+ self.assertEqual([pack.name], sorted(obsolete_names))
336+
337+ def test__save_pack_names_already_obsoleted(self):
338+ tree, r, packs, revs = self.make_packs_and_alt_repo(write_lock=True)
339+ names = packs.names()
340+ pack = packs.get_pack_by_name(names[0])
341+ packs._remove_pack_from_memory(pack)
342+ # We are going to simulate a concurrent autopack by manually obsoleting
343+ # the pack directly.
344+ packs._obsolete_packs([pack])
345+ packs._save_pack_names(clear_obsolete_packs=True,
346+ obsolete_packs=[pack])
347+ cur_packs = packs._pack_transport.list_dir('.')
348+ self.assertEqual([n + '.pack' for n in names[1:]], sorted(cur_packs))
349+ # Note that while we set clear_obsolete_packs=True, it should not
350+ # delete a pack file that we have also scheduled for obsoletion.
351+ obsolete_packs = packs.transport.list_dir('obsolete_packs')
352+ obsolete_names = set([osutils.splitext(n)[0] for n in obsolete_packs])
353+ self.assertEqual([pack.name], sorted(obsolete_names))
354+
355+
356
357 class TestPack(TestCaseWithTransport):
358 """Tests for the Pack object."""

Subscribers

People subscribed via source and target branches