The Problem
When a Zope object does something which is visible outside of the ZODB (i.e. sending email, queuing a print job, updating an external relational database, etc.) and then does something else that generates a ConflictError?, Zope abort()s the transaction and re-runs the code, this then causes the externally visible action to happen over (i.e. two (or three) email's get sent, two (or three) database records get added...)
Possible Solutions
Several possibilities for curing this arise:
- Remove all conflicts, so no Retry calls... This doesn't seem tractable in the short term
- set some sort of visible flag in the REQUEST structure, or in the transaction, or somewhere that indicates that this is a Retried action, that things can check and see that this is a Retry pass, and maybe not do the thing over.
- build an afterCommitHook queue, like the beforeCommitHook queu that exists, and queue up those actions, and run them after (and only if) the commit succeeds.
Of these, I think the last one is the most promising, as it seems to be fairly straightforward, it is similar to an existing mechansm, and the external actions don't happen if we run out of retries. The option of letting the code know it's a Retry means that the action happens, and then if the transaction abort()s a third time, we get the action, but not the bookeeping that says it happens.
In any case, once the mechanism is implemented, and documented, some packages that send mail, etc. should be updated to use it, as should things that do external stuff (CVS interfaces? Database updates?)
Risk Factors
What if code in this afterCommitHook queue itself causes a CommitError??
Scope
I think the circle of packages that should be updated as part of this project should be small, to serve as examples of how to solve this problem for other package maintainers.
Deliverables
I think:
- updated transaction module with afterCommitHook calls mirroring the beforeCommitHook calls
- added code in transaction.commit() to actually call the afterCommitHook code (followed by abort() calls?)
- a utility method queueExternalActivity() that basically calls afterCommitHook()
I now have a NonRetriedActionPatch against zope 2.8.4 for this.