The current code handles transacted (de)installs. However, it does so in a port basis, i.e. a transaction corresponds to the (de)installation of a single port. Why does it work along the dependency chain then? It's because installing the dependencies *is* a part of installing a package. That is, given -> : depends on A -> {B, C}; B -> {D, E}, C -> {D} the ports are installed in that order D E B C A the ports to the right of the place an error/interruption happened are not installed. You can also view this as follows: (repeated letters to show the duration of the install) DDD EEEEE BBBBBBB CCCCCCCCCC AAAAAAAAAAAA i.e. the installation of A is actually a process that involves installing some other ports, so the error propagates naturally error DDD | EEEEEE | BBBBBBBBX | these ports are not installed, but D and E are CCCCCCCCCCC | AAAAAAAAAAAAA | We'd like D and E *not* to be installed, because they're not really wanted and were only installed in the first place in order to be able to get A. This is even more important when we're uninstalling something. Imagine A -> {B, C} Removal is performed this way: AAAAAAAAAAAAAAAA BBBB CCCCCC What if the user cancels during C? AAAAAAAAAAAAAAAA BBBB CCCCX A and C are restored, but B isn't (it was removed completely and the rollback info erased). We reach an unclean state were A is installed but some of its dependencies aren't. Solutions ========= Ad-hoc way ---------- * Infer reverse-dependencies, remove unneeded ports in apply_pending_rollbacks. * Find unfulfilled dependencies, reinstall them. General solution ---------------- Find a way to group individual transactions into a 'über-transaction'. If the über-transaction fails, all its components roll back. In the example above, if the transaction for A were to fail in the final phase (say when running the unit tests on A), all the other sub-transactions should be undone. The problem is that when we start A's transaction we don't know a-priori which subtransactions will be carried out. Ways to solve that: (1) generalize the transaction code so that instead of serializing PackageState objects, a Transaction object is saved to disk, containing all the children. Cons: we have to update the Transaction object repeatedly, it'll be a bit more tricky to do that and guarantee the transaction info is not lost (we woulnd't be able to use the current cheap mechanism and would have to run into 'real' transactions) (2) chain transactions: if we can change the process so that it becomes DDDDDDDDDDDD EEEEEEEEEEEE BBBBBBBBBBBB CCCCCCCCCCCC AAAAAAAAAAAA i.e. the subtransactions are finished when the longest one is finished, the problem is essentially solved. So the problem becomes... how do we chain the transactions (and kind of turn them inside out so that the inner ones depend on the outer one)? Consider now A -> B In the original code, this is what happens (indentation represents nesting levels in method calls): transaction(A) { transaction(B) { .... } ... if something fails here, B, is already installed ... } One attempt do this: transaction(A) { children = [] cont = nil callcc do |cont| transaction(B, cont, children) { callcc do |cc| children << cc ... perform transaction ... cont.call end } end ... | children.each do |c| | callcc do |cont| | c.call # finishes the child transaction | end | end } Have we solved the problem? Not completely. What happens it the code is interrupted during the marked phase?... Another way =========== Not as clever, but actually works: don't remove transaction info in the permanent storage for the subtransactions until the main transaction is finished. It then wipes out all the info of the pending transactions. How could I be so stupid not to see this?