最新消息:ww12345678 的部落格重装上线,希望大家继续支持。

X++, the catch

网络文摘 msmfp 1501浏览 0评论

“When building a cannon, make sure the cannon ball comes out in the right direction.” This is a piece of advice I heard many years ago. I think, we in generally have been following the advice in the Dynamics AX platform group. The APIs and designs have been easy to understand, and without side-effects. This blog post describes an exception – if you are an X++ developer, you better pay attention: The cannon in your hands will not behave as you might expect.

Consider this method:

    void post(int _amount)
{
MyPostings debit;
MyPostings credit;

try
        {
ttsBegin;

debit = MyPostings::find(‘debit’, true);
debit.Amount += _amount;
debit.update();

credit = MyPostings::find(‘credit’, true);
credit.Amount -= _amount;
credit.update();

ttsCommit;
}
catch
        {
error(“Something bad happened – try again later”);
}
}

Is this code safe from a transactional point-of-view? When reading the code, the intention is clear: Either both the debit and credit accounts are updated, or neither are. One of the first things we learned as X++ developers is that exceptions will roll-back the current transaction, and be caught outside the transaction. We also learned that 2 exception types (UpdateConflict and DuplicateKey) can be caught inside the transaction. Regardless of the type of exception, the code is robust, as we’ll never hit the final ttsCommit – so, it appears this code is ok.

Now, this post() method might be so useful and robust, that someone decides to reuse it. For example, like this:

    void postList(List _list)
{
ListEnumerator enum = _list.getEnumerator();

ttsBegin;

while (enum.moveNext());
{
post(enum.current());
}

ttsCommit;
}

Are we still transactional reliable? When reading the code, the intention is clear: Either every item in the list is posted, or none are – by using the post() method that ensures the integrity of each posting.

This code is not reliable – it is a disaster waiting to happen!

Here is what can (and will) happen. Suppose we call the postList() with a list containing the integer 17:

  1. postList() starts a new transaction – ttsLevel is now 1
  2. postList() calls post(17)
  3. post() increases the transaction level – ttsLevel is now 2
  4. post() updates the debit record without problems – this is now part of the transaction.
  5. post() attempts to update the credit record. Suppose this raises an updateConflict exception – someone else has updated the row between the select and the update statement.
  6. The updateConflict is caught by the catch statement in the post() method – YES, catch “all” will catch updateConflict and duplicateKey exceptions, even inside a transaction.
    1. ttsLevel is automatically set back to the value it had on the try-statement – ttsLevel is now 1
    2. The error message is written to the infolog
    3. The post() method ends gracefully – but only the update of debit was added to the transaction
  7. postList() call ttsCommit – ttsLevel is now 0. The update of debit was committed without the update of credit.

The reuse of the post() method resulted in a partial transaction being committed!

Here is the lesson:
Never use catch “all” without explicitly catching updateConflict and duplicateKey exceptions.

One option is to stop using the catch “all” – but often that is not possible. When using catch “all”, you must explicitly also catch updateConflict and duplicateKey. The catch logic for updateConflict/duplicateKey falls in 4 patterns:

1. Playing-it-safe
    catch (Exception::UpdateConflict)
{
if (appl.ttsLevel() != 0)
{
throw Exception::UpdateConflict;
}
if (xSession::currentRetryCount() >= #RetryNum)
{
throw Exception::UpdateConflictNotRecovered;
}
//TODO: Eventual reset of state goes here.
        retry;
}

This is the textbook example, and recommended in the Inside Dynamics AX series. This implementation will escalate the exception to the next level when a transaction is still active. This is a reliable implementation – when your code supports retry. This is the most commonly used pattern, and can be found throughout the standard application. Note: The actual implementation in the text book is slightly different, but semantically the same as mine – I just prefer readable code.

2. I’m-feeling-lucky

    catch (Exception::UpdateConflict)
{
if (xSession::currentRetryCount() >= #RetryNum)
{
throw Exception::UpdateConflictNotRecovered;
}
//TODO: Eventual reset of state goes here.
        retry;
}

The main reason for allowing catching the exception within the transaction is to enable recovery without rolling back everything. This implementation is a bit risky, and should only be used with outmost caution. Only use it when the try block contains just one update statement. The transaction scope will not help you, you need to ensure that entire try block can be repeated – including eventual subscribers to the various events raised. One example where this pattern is used is in InventDim::findOrCreate(). The xSession class has a few helper methods to find the table ID for the table raising the UpdateConflict or DuplicateKey exception – they can be handy in more complicated recovery implementations.

3. Not-my-problem

        catch (Exception::UpdateConflict)
{
throw Exception::UpdateConflict;
}

This is the way to prevent the catch “all” block from catching updateConflicts and duplicateKey exceptions. This is also a reliable implementation – use this when you don’t support retry.

4. I-need-to-log-this

        catch (Exception::UpdateConflict)
{
if (appl.ttsLevel() != 0)
{
throw Exception::UpdateConflict;
}
this.myLogMethod();
}

The myLogMethod() should be called from catch updateConflict, catch duplicateKey and catch “all”. Typically, you only want to log something when you assume you are first on the stack; but add the safe-guard anyway, someone may reuse your method and break that assumption. The BatchRun class contains an example of this.

All that said; there is still the risk of additional exception types, that can be caught inside a transaction, introduced in future versions. None of the patterns above accounts for those. The best way of guarding against this would be to throw an exception in the catch “all” block, if the ttsLevel is not 0. It should be obvious that introducing new exceptions of this type is a breaking change.

What does reliable code look like?

Let’s rewrite the post() method to be reliable using Playing-it-safe pattern, and guarding against future inside-transaction-catchable exceptions.

    void post(int amount)
{
#OCCRetryCount
MyPostings debit;
MyPostings credit;

try
        {
ttsBegin;

debit = MyPostings::find(‘debit’, true);
debit.Amount += amount;
debit.update();

credit = MyPostings::find(‘credit’, true);
credit.Amount -= amount;
credit.update();

ttsCommit;
}
catch (Exception::UpdateConflict)
{
if (appl.ttsLevel() != 0)
{
throw Exception::UpdateConflict;
}
if (xSession::currentRetryCount() >= #RetryNum)
{
throw Exception::UpdateConflictNotRecovered;
}
retry;
}
catch (Exception::DuplicateKeyException)
{
if (appl.ttsLevel() != 0)
{
throw Exception::DuplicateKeyException;
}
if (xSession::currentRetryCount() >= #RetryNum)
{
throw Exception::DuplicateKeyExceptionNotRecovered;
}
retry;
}
catch
        {
if (appl.ttsLevel() != 0)
{
throw error(“Something happened, that the logic was not designed to handle – please log a bug.”);
}
error(“Something bad happened – try again later”);
}
}

What about deadlock exceptions?

Deadlock exceptions cannot be caught inside a transaction – so they essentially behaves like Exception::Error. This means you are not at risk of incomplete transactions, even if they are not caught explicitly.

Typically, the catch of a deadlock simply contains a retry statement – the idea is that once the deadlock transaction is aborted the deadlock situation is lifted, and a retry will succeed. I’ve seen two customer cases of repeatable deadlocks, where the retry statement immediately causes a new deadlock to occur – the system appears to hang, SQL is wasting precious resources to find deadlocks and throw the exception (several times per second), which just prolongs the run for the main thread. I’d propose writing catching of deadlocks like this:

    catch (Exception::Deadlock)
{
if (xSession::currentRetryCount() >= #RetryNum)
{
throw Exception::Deadlock;
}
//TODO: Eventual reset of state goes here.
        retry;
}

Can I catch exception::error instead of catch “all”?

Yes; it would solve the transactional problem – but with a negative side-effect. The downside is that exception::error is just one of many (23 in the latest release) exception types – so you will not be catching the remaining 22. The benefit of cause is that it will not catch updateConflict and duplicateKey exceptions either, so you are avoiding the main problem.

转载请注明:ww12345678 的部落格 | AX Helper » X++, the catch

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址