-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Identify retryable transaction errors and cause them to raise specific exception types #718
Conversation
Hello, thank you for creating this pull request. I have automatically opened an issue http://www.doctrine-project.org/jira/browse/DBAL-1035 We use Jira to track the state of pull requests and the versions they got |
I don't think this should be an additional responsibility of the class RetriableCallable
{
public function __construct(callable $retriable) { ... }
public function __invoke() { ... }
} |
* @link www.doctrine-project.org | ||
* @since 2.5 | ||
*/ | ||
abstract class RetryableException extends ServerException |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't it be better to make it a marker interface instead ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so because catching an interface that is not an exception (since there is not exception interface in php) makes hardly sense since you actually also don't have access to getMessage()
or even getSqlState()
from DriverException. Furthermore Doctrine does not use marker interfaces for exceptions (at least I didn't find any).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Furthermore Doctrine does not use marker interfaces for exceptions
That's actually something that we should start doing, heh...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would even go further and say we should have something like TransactionException extends ServerException
to have a base class for all transaction exceptions. Then DeadlockException
and LockWaitTimeoutException
should implement the marker interface RetryableException
. Another idea would be to name that RetryableException
ConcurrencyException
(because that's what it actually is) although I'm not sure whether every concurrency exception can be retried. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything is a transaction in database terms. So what's the point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Tobion Catching on an interface still gives you access to getMessage()
, given that the fact that it is a catch block tells you that the object is also an exception, not only an instance of this interface (the fact that IDEs don't support autocompletion properly in such case is not an issue in the usage of an exception for that, but in the inference engine of the IDE)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true, I changed it
* @throws \Exception If an exception has been raised where retrying makes no sense | ||
* or a RetryableException after max retries has been reached. | ||
*/ | ||
public function retryable(Closure $func, $maxRetries = 3, $retryDelay = 100) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should allow any callables (we have Closure
typehints in several places in Doctrine because of a bad decision years ago and the fact that breaking BC to change the typehint is not worth a major version by itself, so it will wait until a real need for a major version)
+1 I see it following patterns of current Doctrine standards. If different patterns should be used, thats what the next version is for IMHO. Looking forward to using this! |
@ChristianRiesen if we start introducing new APIs now then the migration path for 3.x will just be harder. Let's not break SRP for the sake of usability if it's just going to damage us. |
I see your argument @Ocramius but by that definition any pull request that isn't a bug fix or essentially, a 3.0 enhancement, is mute. That would hurt the doctrine project more imho. |
Thinking about it, I feel @Ocramius is right that it does not necessarily belong on the Connection. So I'm willing to change it to an invokable class. How about |
what about a decorator class around the Connection? |
Why is this sort of API in first place a responsibility of the |
Maybe we should consider integrating this feature into #634 instead? Not sure if there is a better place for this functionality... |
The class name is |
@Tobion RetryDecorator would not be more correct, given that it is not a decorator. If you want a more correct name, |
@stof Why should it not be a decorator? It decorates a callable as a callable that adds functionality (retry logic). This is exactly the definition of the decorator pattern. |
} | ||
} | ||
|
||
class DummyDriverException implements DriverException |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this needs to extends Exception, otherwise weird things will happen
Thinking about it, the RetryWrapper is actually totally generic and the use-case would not be limited to database operations. For example you can also use it for instable http operations. So actually it might be worth extracting it into a separate library, similar to https://github.com/nfedyashev/retryable in ruby. |
I doubt that we would include an external dependency for such a small library right now |
It could be an optional dependency which would only be required when you need the retry logic for your db operations. |
It cannot be optional if doctrine extensions are implementing the marker interface of the library |
The marker interface would not need to be part of the library. The library would catch all exceptions by default and allow to configure to retry only on specific ones (as in the ruby library), like RetryableException in DBAL. |
for what it is worth, https://github.com/igorw/retry just misses the possibility to filter which exceptions should be retried |
Hm I didn't know about this one. But it also has no delay. Maybe @igorw and me should put our efforts together. |
@Tobion the delay is still in a PR because there is a discussion about the way to implement it |
@Ocramius @stof I removed the retry logic from the PR as this can be provided by another package (https://github.com/Tobion/retry). So this PR now only contains the explicit exceptions so they can be catched RDMBS agnostic. Please consider merging. |
@deeky666 ping |
@deeky666 any schedule for this? |
I'd like to know the same. This has been open forever. |
ping |
Identify retryable transaction errors
@Tobion @ChristianRiesen I'm sorry for the delay, haven't been very active during the last year. Merging this now. Will land in |
Any chance this change would get tagged soon? |
+1 |
So this PR now only contains the explicit exceptions so they can be catched RDMBS agnostic.
Below description can be provided externally.
It is best practice to implement retry logic for transactions that are aborted because of deadlocks or timeouts. This makes such method available inside the DBAL and also adds detection for errors where retrying makes sense in the different database drivers.
Deadlocks and timeouts are caused by lock contention and you often can design your application to reduce the likeliness that such an error occurs. But it's impossible to guarantee that such error conditions will never occur. This is why implementing retrying logic for such errors is actually a must when you have to ensure the application does not fail in edge cases or high load.
Some references where something similar has already been discussed and implemented:
I chose the name
retryable
because it is consistent withtransactional
. I think the implementation is quite straight forward and fits very well with the DBAL design.In our case we had seldomly errors like
Doctrine\\DBAL\\Exception\\DriverException: An exception occurred while executing 'UPDATE product SET modified = ? WHERE id = ?' with params [\"2014-10-15 16:28:55\", \"460315800000\"]:\n\nSQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction
Doctrine\\DBAL\\Exception\\DriverException: An exception occurred while executing 'INSERT INTO ... VALUES (...)' with params [\"...\", \"...\"]:\n\nSQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
As you can see even the exception message suggests to retry the transaction. This is now easily possible with