N
N
nepster-web2015-12-31 15:26:16
Yii
nepster-web, 2015-12-31 15:26:16

How do transaction failures occur?

All with the upcoming!
They gave me a New Year's gift. The project has the ability to transfer funds from one user to another. The fact is that such a situation occurred:
There was one smart-ass user who magically threw funds to another account so that they were not withdrawn from his account.
The whole site is written in Yii2. There is a form model inherited from base / model, validation of each sneeze, including the time of the last transfer, that is, you cannot transfer funds more than once every 30 seconds.
The very logic of the transfer of funds:

/**
     * Создать новую заявку на перевод средств
     * @throws \Exception
     * @throws \yii\db\Exception
     * @return bool
     */
    public function create()
    {
        $transaction = Yii::$app->db->beginTransaction();
        try {

            $transfer = new Transfer();
            $transfer->user_sender = Yii::$app->user->id;
            $transfer->user_recipient = $this->_user_recipient;
            $transfer->time_create = time();
            $transfer->funds = $this->amount;
            $transfer->status = Transfer::STATUS_PENDING;
            $transfer->comm = $this->comm;
            $transfer->save(false);

            Yii::$app->balance
                ->setModule('transfer')
                ->setUser(Yii::$app->user->identity)
                ->setEntityId($transfer->id)
                ->costs($transfer->funds, Transfer::TYPE_SUCCESS);

            // Перевод без подтверждения
            if (!Yii::$app->config->get('transferModeration')) {
                $transfer->time_process = time();
                $transfer->status = Transfer::STATUS_SUCCESS;

                Yii::$app->balance
                    ->setModule('transfer')
                    ->setUser(User::findOne($this->_user_recipient))
                    ->setEntityId($transfer->id)
                    ->billing($transfer->funds, Transfer::TYPE_SUCCESS);
            }

            $transfer->save(false);

            $transaction->commit();
            return true;
        } catch(\Exception $e) {
            $transaction->rollBack();
            return false;
        }
    }

The logic of the balance component (the main point is that we either add or subtract funds from the account and create a record in the history):
...
        // Было средств на счету
        $was = $this->_user->$account;

        $model = $this->_model;

        if ($type == $model::DEPOSIT || $type == $model::BILLING) {
            $this->_user->$account += $amount;
        }
        else {
            $this->_user->$account -= $amount;
        }

        // Обновляем счет пользователя
        $this->_user->save(false);

        // Запись в историю денежного оборота
        $result = call_user_func_array(
            [$model, $this->_method], [
                $this->_module,
                $type,
                $system,
                $this->_user->id,
                $was,
                $amount,
                $this->_entityId,
                $this->_data,
        ]);

        return $result;

As you can see from the above form, everything is done in a transaction, which should guarantee the integrity of the data.
However, if you look at the database:
cc07a0a8499f4e30a981dfba8e89140c.png
You can see that the time of creating a transfer request is the same. How is this possible if there is a validation for a while (that is, the last application gets and the time is checked).
As a result, one of the requests goes through correctly, while the second misses part of the logic and does not write off funds. How is this possible ?
I did not succeed in repeating this as a debug. Pressing f5, logging out during the payment process, etc.

Answer the question

In order to leave comments, you need to log in

2 answer(s)
D
Dmitry Donkovtsev, 2015-12-31
@nepster-web

If I don't confuse anything, then:
1) $transfer->save(false); - you have disabled validation when saving models.
2) When committing, you do not check that the result of saving all models is positive (true), and it’s stupid that you can’t validate anywhere.
3) When testing, have you tried sending a negative number? Perhaps it is worth checking that the value is a number, but that it is not worth more than 0, this can lead to problems.
All this, of course, is not a solution to your problem, but rather something that you can pay attention to.

P
pantsarny, 2016-01-07
@pantsarny

if ($type == $model::DEPOSIT || $type == $model::BILLING) {
            $this->_user->$account += $amount;
        }
        else {
            $this->_user->$account -= $amount;
        }

        // Обновляем счет пользователя
        $this->_user->save(false);

Here is the trick. The previous balance is stored in a variable, then increment/decrement the variable and write the new value.
Yii makes a query like
UPDATE user SET balance = $new_balance...
And you need to make a query like
UPDATE user SET balance = balance - $amount WHERE balance >= $amount AND user_id = 1
And see if the user's balance is greater than the amount he wants to conduct, then the request will return one to you, i.e. one post edited. Then you can add the balance to the second user, otherwise there is a catch and we do not approve the transfer.
UPDATE user SET balance = balance + $amount WHERE user_id = 2
And then save the listing entry itself to a table. We turn the whole thing into a transaction and are happy.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question