You do not know how to work with transactions.
The headline came catchy, but boiling. I must say that it will be about 1C. Dear 1C-nicks, you do not know how to work with transactions and do not understand what exceptions are. I came to this conclusion by looking at a large amount of code on 1C, born in the wilds of a domestic enterprise. In typical configurations, this is pretty good, but the horrendous amount of custom code is written incompetently in terms of working with the database. Have you ever seen the error "Error already occurred in this transaction"? If so, then the title of the article applies to you. Let's see under the cut, finally, what are transactions and how to handle them correctly, working with 1C.
Why do I need to sound the alarm
To begin with, let's see what is the error "There have already been errors in this transaction." This is actually a very simple thing: you are trying to work with a database inside an already rolled back (canceled) transaction. For example, the CancelTransaction method was called somewhere, and you are trying to commit it.
Why is that bad? Because this error tells you nothing about where the problem actually happened. When a user receives a screenshot with such text, and especially for server code that the person does not work with interactively, this ... I wanted to write a "critical error", but I thought it was a buzzword that nobody already pays attention to .... This is ass. This is a programming error. This is not an occasional crash. This cant, which must be redone immediately. Because when your server’s background processes get up at night and the company starts to lose money fast, then “There have already been errors in this transaction” is the last thing you want to see in the diagnostic logs.
There is, of course, the possibility that the server’s technological log (it’s included in production, right?) Will somehow help diagnose the problem, but I can’t think of a variant right now - how to find the real cause of this error in it. But the real reason is one - programmer Vasya received an exception inside the transaction and decided thatonce - not a carabas "think, mistake, let's go further."
What is a transaction in 1C
It is embarrassing to write about truism, but, probably, it is necessary a little. Transactions in 1C are the same as transactions in a DBMS. These are not some special "1C-s" transactions, these are transactions in a DBMS. According to the general idea of transactions, they can either be executed entirely or not executed at all. All changes to database tables made within a transaction can be undone at once, as if nothing had happened.
Further, you need to understand that nested transactions are not supported in 1C. In fact, they are not supported not "in 1C", but are not supported at all. At least, those DBMS with which 1C can work. Nested transactions, for example, are not in MS SQL and Postgres. Each "nested" call to Start Transaction simply increases the transaction counter, and each call to "Commit Transaction" reduces this counter. This behavior is described in many books and articles, but the conclusions from this behavior, apparently, are not well analyzed. Strictly speaking, in SQL there is a so-called. SAVEPOINT, but 1C does not use them, and the thing is quite specific.
Hereinafter, especially for the Warriors of the True Faith, who believe that the code should be written only in English, an analogue of the code in English syntax 1C will be given under the spoilers.
ПроцедураОченьПолезныйИВажныйКод(СписокСсылокСправочника)
НачатьТранзакцию();
ДляКаждого Ссылка Из СписокСсылокСправочника Цикл
ОбъектСправочника = Ссылка.ПолучитьОбъект();
ОбъектСправочника.КакоеТоПоле = "Я изменен из программного кода";
ОбъектСправочника.Записать();
КонецЦикла;
ЗафиксироватьТранзакцию();
КонецПроцедуры
На самом деле, нет. Мне совершенно не хочется дублировать примеры на английском только ради того, чтобы потешить любителей холиваров и священных войн.
You probably write this code, right? The code example below contains errors. At least three. Do you know what? About the first, I will say right away, it is associated with object locks and is not directly related to transactions. About the second - a little later. The third mistake is a deadlock, which will arise when the code is executed in parallel, but this is a topic for a separate article, we will not consider it now, so as not to complicate the code. Keyword for googling: deadlock managed locks .
Notice that the code is simple. This in your 1C systems is just a car. And it immediately contains at least 3 errors. Think at your leisure, how many errors are there in more complex transaction scenarios written by your 1C programmers :)
Object locks
So the first mistake. In 1C there are object locks, the so-called "optimistic" and "pessimistic". Who came up with the term, I do not know, I would have killed :). It’s absolutely impossible to remember which one is responsible for what. Details about them are written here and here , as well as in other general-purpose IT literature.
The essence of the problem is that in the specified example code the database object changes, but in another session an interactive user (or a neighboring background thread) can sit, who will also change this object. Here one of you may get the error "the record has been changed or deleted." If this happens in an interactive session, the user will scratch the turnip, swear, and try to rediscover the form. If this happens in the background thread, then you will have to look for it in the logs. And, as you know, the registration log is slow, and in the industry, we are setting up an ELK stack for 1C magazines ... (we, by the way, are among those who tune and help others to tune :))
In short, this is an annoying mistake and it is better that it is not. Therefore, in the development standards it is clearly written that before changing the objects it is necessary to put an object lock on them using the " Object Reference Guide . Block () ". Then a parallel session (which should also do this) will not be able to start a change operation and will receive the expected, controlled failure.
And now about transactions
With the first error figured out, let's move on to the second.
If you do not provide for an exception check in this method, then an exception (for example, very likely on the "Write ()" method) will throw you out of this method without completing the transaction . An exception to the "Write" method may be thrown out for a variety of reasons, for example, some application checks in the business logic will work, or the object lock mentioned above will occur. Anyway, the second error says: the code that started the transaction is not responsible for its completion.
That is what I would call this problem. In our static analyzer code 1C based on SonarQube, we even separately embedded such diagnostics. Now I am working on its development, and the imagination of 1C programmers, whose code gets into my analysis, sometimes leads me to shock and awe ...
Why? Because the exception thrown up inside the transaction in 90% of cases will not allow this transaction to be fixed and will result in an error. It should be understood that 1C automatically rolls back an incomplete transaction only after returning from the script code to the platform code level. As long as you are at the level of code 1C, the transaction remains active.
We will rise to a higher level in the call stack:
ПроцедураВажныйКод()
СписокСсылок = ПолучитьГдеТоСписокСсылок();
ОченьПолезныйИВажныйКод(СписокСсылок);
КонецПроцедуры
See what happens. Our problem method is called from somewhere outside, upstream. At the level of this method, the developer has no idea whether there will be any transactions inside the Very Utility method and an Important Code or not. And if they do, then they will all be completed ... We are all here for peace and encapsulation, right? The author of the "Important Code" method should not think about what exactly happens inside the method it calls. That in which transaction is incorrectly processed. As a result, an attempt to work with the database after an exception is thrown from inside the transaction will most likely result in "In this transaction, blah blah ..."
Transaction spreading by methods
The second rule of the "transaction-safe" code: the transaction reference count at the beginning of the method and at the end of it must have the same value . You cannot start a transaction in one method and end it in another. From this rule, you can probably find exceptions, but it will be some kind of low-level code that is written by more competent people. In general, it is impossible to write this way.
For example:
ПроцедураВажныйКод()
СписокСсылок = ПолучитьГдеТоСписокСсылок();
ОченьПолезныйИВажныйКод(СписокСсылок);
ЗафиксироватьТранзакцию();
// Путевка в ад, серьезный разговор с автором о наших сложных трудовых отношениях.КонецПроцедуры
Above - unacceptable govnokod. You cannot write methods so that the caller remembers and monitors the possible (or probable — who knows) transactions within the other methods that it calls. This is a violation of encapsulation and the growth of spaghetti code, which can not be traced, while maintaining sanity.
It is especially fun to remember that the real code is much more synthetic examples from 3 lines. To seek out the beginning and ending transactions at six levels of nesting - this directly motivates to intimate conversations with the authors.
Trying to fix the code
Let's return to the original method and try to fix it. At once I will say that we will not fix the object lock for the time being, just so as not to complicate the example code.
The first approach is a typical 1C-nickname
Usually, 1C programmers know that an exception may be thrown when writing. And they are afraid of exceptions, so they try to intercept them. For example, like this:
ПроцедураОченьПолезныйИВажныйКод(СписокСсылокСправочника)
НачатьТранзакцию();
ДляКаждого Ссылка Из СписокСсылокСправочника Цикл
ОбъектСправочника = Ссылка.ПолучитьОбъект();
ОбъектСправочника.КакоеТоПоле = "Я изменен из программного кода";
Попытка
ОбъектСправочника.Записать();
ИсключениеЛог.Ошибка("Не удалось записать элемент %1", Ссылка);
Продолжить;
КонецПопытки;
КонецЦикла;
ЗафиксироватьТранзакцию();
КонецПроцедуры
Well, it got better, right? After all, now, possible write errors are processed and even logged. Exceptions will no longer occur when recording an object. And you can even see in the log - on which object, I was not lazy, I put a link in the message instead of a concise "Directory writing error", as developers always rush to like to write. In other words, there is a concern for the user and the growth of competencies.
However, an experienced 1C-nick here will say that no, it has not become better. In fact, nothing has changed, and maybe even worse. In the "Record ()" method, the 1C platform itself will start a record transaction, and this transaction will be already nested in relation to ours. And if at the time of working with the database 1C, its transaction is rolled back (for example, an exception of business logic will be issued), then our top-level transaction will still be marked as “corrupted” and cannot be fixed. As a result, this code will remain problematic, and when attempting to commit it will produce "errors have already occurred."
Now imagine that this is not about a small method, but about a deep stack of calls, where at the very bottom someone took and “released” the started transaction from his method. Top-level procedures may not have a clue that someone down there started a transaction. As a result, all the code falls with an indistinct error, which is impossible to investigate in principle.
The code that starts a transaction is required to complete or roll it back. Regardless of any exceptions. Each code branch should be examined to exit the method without committing or canceling a transaction.
Methods of working with transactions in 1C
It will not be superfluous to remind that in general 1C provides us with work with transactions. These are well-known methods:
- Start Transaction ()
- Commit Transaction ()
- CancelTransaction ()
- TransactionActive ()
The first 3 methods are obvious and do what is written in their names. The last method returns True if the transaction count is greater than zero.
And there is an interesting feature. Transaction exit methods (Commit and Cancel) throw out exceptions if the transaction count is zero. That is, if you call one of them outside the transaction, an error will occur.
How to use these methods? Very simple: you must read the rule formulated above: the code that started the transaction should be responsible for completing it.
How to observe this rule? Let's try:
НачатьТранзакцию();
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Above, we already understood that the method of Doing What - is potentially dangerous. It can throw some kind of exception, and the transaction will “get out” of our method. Ok, let's add a possible exception handler:
НачатьТранзакцию();
Попытка
ДелаемЧтоТо();
Исключение// а что же написать тут?КонецПопытки;
ЗафиксироватьТранзакцию();
Great, we caught the error, but what to do with it? Write a message to the log? Well, maybe, if the error logging code should be at this level and we are waiting for the error. And if not? If we did not expect any mistakes here? Then we should just pass this exception higher, let another layer of architecture deal with them. This is done by the operator "Call Exception" without arguments. In these java-siplus ones, this is done in the same way with the throw operator.
НачатьТранзакцию();
Попытка
ДелаемЧтоТо();
ИсключениеВызватьИсключение;
КонецПопытки;
ЗафиксироватьТранзакцию();
So, stop ... If we just throw an exception further, then why is there an Attempt at all? But why: the rule forces us to ensure the completion of the transaction we started.
НачатьТранзакцию();
Попытка
ДелаемЧтоТо();
ИсключениеОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
ЗафиксироватьТранзакцию();
Now, seemingly, beautiful. However, we remember that we do not trust the code. We do what (). Suddenly, there inside its author did not read this article, and does not know how to work with transactions? Suddenly he took it there, and called the CancelTransaction method or, on the contrary, fixed it? It is very important for us that the exception handler does not generate a new exception , otherwise the original error will be lost and the investigation of problems will become impossible. And we remember that the Commit and Cancel methods can throw an exception if the transaction does not exist. This is where the TransactionActive method comes in handy.
Final option
Finally, we can write the correct, "transaction-safe" version of the code. Here he is:
** UPD: in the comments, a more secure option is proposed when the Commit Transaction is located inside the Attempt block. Here is exactly this option; previously, Fixation was located after the Attempt-Exclusion block.
НачатьТранзакцию();
Попытка
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
ИсключениеЕслиТранзакцияАктивна() ТогдаОтменитьТранзакцию();
КонецЕсли;
ВызватьИсключение;
КонецПопытки;
Wait, but not only "Cancel Transaction" may produce errors. Why, then, "Commit Transaction" is not wrapped in the same condition with "Transaction Active"? Again, by the same rule: the code that initiated the transaction should be responsible for completing it. Our transaction is not necessarily the first, it can be nested. At our level of abstraction, we are obliged to care only about our transaction. All others should be uninteresting to us. They are strangers, we should not be responsible for them. It is NOT MUST. No attempt should be made to ascertain the real level of the transaction counter. This again breaks the encapsulation and causes the transaction management logic to "blur". We checked the activity only in the exception handler and only towill not raise a new exception, "hiding" the old .
Refactoring checklist
Let's look at some of the most common situations that require intervention in the code.
Pattern:
НачатьТранзакцию();
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Wrap in a "safe" design with Attempt, Activity Check and forwarding exceptions.
Pattern:
ЕслиНеТранзакцияАктивна() ТогдаНачатьТранзакцию()
КонецЕсли
Analysis and Refactoring. The author did not understand what he was doing. Starting nested transactions is safe. You do not need to check the condition, you just need to start the nested transaction. Lower down the module, he probably still perverts there with their fixation. This is a guaranteed hemorrhoids.
Approximately similar option:
ЕслиТранзакцияАктивна() ТогдаЗафиксироватьТранзакцию()
КонецЕсли
Similarly: fixing a transaction by condition is strange. Why is there a condition? What, someone else could already commit this transaction? The reason for the proceedings.
Pattern:
НачатьТранзакцию()
Пока Выборка.Следующий() Цикл// чтение объекта по ссылке// запись объектаКонецЦикла;
ЗафиксироватьТранзакцию();
- enter a controlled lock to avoid deadlock
- enter a call to the Block method
- wrap in “try” as shown above
Pattern:
НачатьТранзакцию()
Пока Выборка.Следующий() ЦиклПопытка
Объект.Записать();
ИсключениеСообщить("Не получилось записать");
КонецПопытки;
КонецЦикла;
ЗафиксироватьТранзакцию();
This transaction will not complete in the event of an exception. It makes no sense to continue the cycle. Code needs to be rewritten, referring to the original task. Additionally provide a more informative error message.
Finally
I, as you have probably guessed, are people who love the 1C platform and the development on it. Of course, there are complaints about the platform, especially in the Highload environment, but in general, it allows you to inexpensively and quickly develop very high-quality enterprise applications. Giving out of the box both ORM, and GUI, and a web interface, and Reporting, and much more. In the comments on Habré, everyone writes something arrogant, and so, the guys are the main problem of 1C, as ecosystems are not a platform or a vendor. This is a too low entry threshold that allows people who do not understand what a computer, a database, a client server, a network, and all that, to enter the industry. 1C has made the development of enterprise applications too easy. In 20 minutes I can write on it an accounting system for purchases / sales with flexible reports and a web client. Thereafter, it is easy for me to think about myself, that on a large scale you can write about the same. Somehow there 1C itself will do everything inside, I do not know how, but I will probably do it. I'll write "Start Transaction ()" ....
And you know - the most important thing is that it is beautiful. Ease of development in 1C allows you to instantly implement business ideas and embed them in the processes of the company. Then you can always refactor, the main thing is to understand how. And if suddenly you need help in auditing your “slow 1C” - contact the optimization specialists. She is not slow at all.