diff --git a/.bsl-language-server.json b/.bsl-language-server.json
index 09732ad5..87afa8c9 100644
--- a/.bsl-language-server.json
+++ b/.bsl-language-server.json
@@ -5,7 +5,7 @@
"listOfIncorrectFirstSymbol": ";|,\\s*\\S+"
},
"Typo": {
- "userWordsToIgnore": "переиспользование,Переиспользование,переиспользования,Переиспользования,переиспользованию,Переиспользованию,переиспользованием,Переиспользованием,переиспользовано,Переиспользовано,переиспользовать,Переиспользовать,переиспользует,Переиспользует,переиспользуется,Переиспользуется,переиспользуются,Переиспользуются"
+ "userWordsToIgnore": "Фикстуры,фикстуры,Валидатор,валидатор,валидатора,Валидатора,валидатором,Валидатором,переиспользование,Переиспользование,переиспользования,Переиспользования,переиспользованию,Переиспользованию,переиспользованием,Переиспользованием,переиспользовано,Переиспользовано,переиспользовать,Переиспользовать,переиспользует,Переиспользует,переиспользуется,Переиспользуется,переиспользуются,Переиспользуются,Переподписывающийся,переподписывающийся"
}
}
}
diff --git a/README.md b/README.md
index 4b75acbf..09f7894e 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,7 @@
* Активная запись
* Работа через ХранилищеСущностей
* Работа с транзакциями
+ * Наблюдатели источника данных
* Система аннотаций для сущностей
* Сущность
* Идентификатор
@@ -394,6 +395,49 @@
МенеджерСущностей.ЗафиксироватьТранзакцию();
```
+
+
+## Наблюдатели источника данных
+
+Источник данных, с которым работает менеджер сущностей, сообщает зарегистрированным наблюдателям
+обо всем, что с ним происходит: об операциях (Сохранить, Получить, Удалить и других), запросах к
+СУБД, захвате и освобождении соединений и транзакциях. Каждое событие приходит наблюдателю дважды -
+до выполнения и после - одним объектом `СобытиеИсточникаДанных`, в котором после выполнения есть
+длительность, ошибка и результат. На этом строятся телеметрия, журнал медленных запросов, подсчет
+обращений к БД в тестах.
+
+Наблюдатель объявляет интерфейс `НаблюдательИсточникаДанных` аннотацией на конструкторе и
+реализует два метода:
+
+```bsl
+&Реализует("НаблюдательИсточникаДанных")
+Процедура ПриСозданииОбъекта()
+КонецПроцедуры
+
+Процедура ПередСобытием(Событие) Экспорт
+КонецПроцедуры
+
+Процедура ПослеСобытия(Событие) Экспорт
+ Если Событие.Вид() = ВидыСобытийИсточникаДанных.Запрос() И Событие.Длительность() > 100 Тогда
+ Сообщить(СтрШаблон("Медленный запрос к %1: %2 мс", Событие.ИмяТаблицы(), Событие.Длительность()));
+ КонецЕсли;
+КонецПроцедуры
+```
+
+Регистрируется наблюдатель на источнике данных, а менеджер создается из источника и отдает
+наблюдателю все, что с источником происходит: операции самого менеджера, его хранилищ сущностей
+и активной записи, запросы, соединения и транзакции.
+
+```bsl
+Источник = Новый ИсточникДанных("Основной", Тип("КоннекторSQLite"), СтрокаСоединения);
+Источник.ДобавитьНаблюдателя(Новый МедленныеЗапросы);
+МенеджерСущностей = Новый МенеджерСущностей(Источник);
+```
+
+Виды событий, их поля, поведение при ошибках и в нескольких потоках, а также поддержка
+наблюдателей в собственном коннекторе описаны в разделе
+[Наблюдатели источника данных](docs/Наблюдатели.md).
+
## Система аннотаций для сущностей
@@ -528,6 +572,8 @@
Например, `КоннекторJSON` не умеет работать с транзакциями, однако, он имеет соответствующие методы, выводящие диагностические сообщения при их вызове.
+Коннектор **может** дополнительно реализовать интерфейс [`НаблюдаемыйКоннектор`](docs/НаблюдаемыйКоннектор.md): тогда наблюдатели источника данных получают события его запросов к СУБД и описание соединения. Встроенные коннекторы реализуют оба интерфейса. Коннектор без второго интерфейса работает как раньше.
+
> Важно!
Менеджеры и хранилища сущностей одного источника данных собственных коннекторов не держат: они берут соединения из общего пула этого источника. Тип, строка соединения и параметры коннектора задаются при создании ИсточникаДанных.
@@ -646,14 +692,18 @@
* [`ОбъектМодели`](src/Классы/ОбъектМодели.os),
* [`ЭлементОтбора`](src/Классы/ЭлементОтбора.os);
* [`ЭлементПорядка`](src/Классы/ЭлементПорядка.os);
+ * [`НаблюдательИсточникаДанных`](src/Классы/НаблюдательИсточникаДанных.os);
+ * [`СобытиеИсточникаДанных`](src/Классы/СобытиеИсточникаДанных.os);
* значения модулей-перечислений:
* [`ТипыКолонок`](src/Модули/ТипыКолонок.os),
* [`ТипыПодчиненныхТаблиц`](src/Модули/ТипыПодчиненныхТаблиц.os),
- * [`ВидСравнения`](src/Модули/ВидСравнения.os);
+ * [`ВидСравнения`](src/Модули/ВидСравнения.os),
+ * [`ВидыСобытийИсточникаДанных`](src/Модули/ВидыСобытийИсточникаДанных.os);
* состав и параметры аннотаций сущностей;
* методы ["активной записи"](docs/АктивнаяЗапись.md) сущности;
* для Major.Connector:
* все публичные методы класса [`АбстрактныйКоннектор`](src/Классы/АбстрактныйКоннектор.os) и их сигнатуры;
+ * все публичные методы необязательного интерфейса [`НаблюдаемыйКоннектор`](src/Классы/НаблюдаемыйКоннектор.os) и их сигнатуры, а также методы `НачатьЗапрос` и `ЗавершитьЗапрос` реестра наблюдателей, который коннектор получает в `УстановитьНаблюдателей`;
* все публичные непомеченные как "нестабильные" (`@unstable`) методы классов:
* [`МодельДанных`](src/Классы/МодельДанных.os),
* [`ОбъектМодели`](src/Классы/ОбъектМодели.os),
@@ -663,6 +713,7 @@
* значения модулей-перечислений:
* [`ТипыКолонок`](src/Модули/ТипыКолонок.os),
* [`ТипыПодчиненныхТаблиц`](src/Модули/ТипыПодчиненныхТаблиц.os),
- * [`ВидСравнения`](src/Модули/ВидСравнения.os).
+ * [`ВидСравнения`](src/Модули/ВидСравнения.os),
+ * [`ВидыСобытийИсточникаДанных`](src/Модули/ВидыСобытийИсточникаДанных.os).
> To be continued...
diff --git a/docs/README.md b/docs/README.md
index a6488ac3..3291825f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -13,13 +13,18 @@
* [ОбъектМодели](ОбъектМодели.md)
* [ЭлементОтбора](ЭлементОтбора.md)
* [ЭлементПорядка](ЭлементПорядка.md)
+* [НаблюдательИсточникаДанных](НаблюдательИсточникаДанных.md)
+* [НаблюдаемыйКоннектор](НаблюдаемыйКоннектор.md)
+* [СобытиеИсточникаДанных](СобытиеИсточникаДанных.md)
### Перечисления
* [ВидСравнения](ВидСравнения.md)
+* [ВидыСобытийИсточникаДанных](ВидыСобытийИсточникаДанных.md)
* [ТипыКолонок](ТипыКолонок.md)
* [ТипыПодчиненныхТаблиц](ТипыПодчиненныхТаблиц.md)
### Прочее
* [Потокобезопасная работа с БД](ПотокобезопаснаяРаботаСБД.md)
+* [Наблюдатели источника данных](Наблюдатели.md)
diff --git "a/docs/\320\222\320\270\320\264\321\213\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.md" "b/docs/\320\222\320\270\320\264\321\213\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.md"
new file mode 100644
index 00000000..23f21169
--- /dev/null
+++ "b/docs/\320\222\320\270\320\264\321\213\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.md"
@@ -0,0 +1,65 @@
+## ВидыСобытийИсточникаДанных
+
+Виды событий, которые источник данных отдает наблюдателям, см. `Вид()` у
+[СобытиеИсточникаДанных](СобытиеИсточникаДанных.md). Значения отдаются функциями, а не экспортными
+переменными, чтобы их нельзя было изменить извне. Наблюдатель сравнивает вид события с ними,
+а не со строковыми литералами:
+
+```bsl
+Если Событие.Вид() = ВидыСобытийИсточникаДанных.Запрос() Тогда
+```
+
+### Операция
+
+```bsl
+// Операция менеджера или хранилища: Сохранить, Получить, Удалить и другие.
+//
+// Возвращаемое значение:
+// Строка - Вид события операции.
+//
+Функция Операция() Экспорт
+```
+
+### Запрос
+
+```bsl
+// Запрос коннектора к СУБД: SELECT, INSERT, DELETE, CREATE TABLE, BEGIN, COMMIT, ROLLBACK.
+//
+// Возвращаемое значение:
+// Строка - Вид события запроса.
+//
+Функция Запрос() Экспорт
+```
+
+### Соединение
+
+```bsl
+// Захват и освобождение соединения пула: Занять, Освободить.
+//
+// Возвращаемое значение:
+// Строка - Вид события соединения.
+//
+Функция Соединение() Экспорт
+```
+
+### Транзакция
+
+```bsl
+// Транзакция потока исполнения от начала до исхода.
+//
+// Возвращаемое значение:
+// Строка - Вид события транзакции.
+//
+Функция Транзакция() Экспорт
+```
+
+### Виды
+
+```bsl
+// Все виды событий в порядке объявления.
+//
+// Возвращаемое значение:
+// ФиксированныйМассив из Строка - Операция, Запрос, Соединение, Транзакция.
+//
+Функция Виды() Экспорт
+```
diff --git "a/docs/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.md" "b/docs/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.md"
index f6c25745..e0cf8174 100644
--- "a/docs/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.md"
+++ "b/docs/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.md"
@@ -13,8 +13,14 @@
```bsl
Источник = Новый ИсточникДанных("Основной", Тип("КоннекторPostgreSQL"), СтрокаСоединения, , 20);
+Источник.ДобавитьНаблюдателя(Новый МойНаблюдатель);
+
+МенеджерСущностей = Новый МенеджерСущностей(Источник);
```
+Реестр наблюдателей общий у источника, его пула и всех менеджеров, созданных из него, см.
+[Наблюдатели источника данных](Наблюдатели.md).
+
### ПриСозданииОбъекта
```bsl
@@ -150,3 +156,47 @@
//
Функция ПулСоединений() Экспорт
```
+
+### ДобавитьНаблюдателя
+
+```bsl
+// Регистрирует наблюдателя источника данных.
+//
+// Наблюдатель получает события всех операций, запросов, соединений и транзакций каждого
+// менеджера, созданного из этого источника: самого менеджера, его хранилищ сущностей и
+// Active Record. Реестр общий у источника и его менеджеров, поэтому регистрировать можно
+// и после создания менеджера. См. интерфейс НаблюдательИсточникаДанных и класс
+// СобытиеИсточникаДанных.
+//
+// Параметры:
+// Наблюдатель - Произвольный - Объект с аннотацией &Реализует("НаблюдательИсточникаДанных") на конструкторе,
+// реализующий этот интерфейс. Неполная реализация отвергается при регистрации.
+//
+// Пример:
+// Источник.ДобавитьНаблюдателя(Новый МойНаблюдатель);
+//
+Процедура ДобавитьНаблюдателя(Наблюдатель) Экспорт
+```
+
+### УдалитьНаблюдателя
+
+```bsl
+// Снимает наблюдателя источника данных с регистрации.
+//
+// Параметры:
+// Наблюдатель - Произвольный - Ранее зарегистрированный наблюдатель. Незарегистрированный пропускается.
+//
+Процедура УдалитьНаблюдателя(Наблюдатель) Экспорт
+```
+
+### Наблюдатели
+
+```bsl
+// @internal
+// Реестр наблюдателей источника: источник передает его своему пулу соединений.
+//
+// Возвращаемое значение:
+// НаблюдателиИсточникаДанных - Реестр наблюдателей.
+//
+Функция Наблюдатели() Экспорт
+```
diff --git "a/docs/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.md" "b/docs/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.md"
index 2e373405..dd16e32b 100644
--- "a/docs/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.md"
+++ "b/docs/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.md"
@@ -6,14 +6,15 @@
// Конструктор объекта МенеджерСущностей.
//
// Менеджер создается из источника данных и берет соединения из его пула: все менеджеры одного
-// источника делят соединения и транзакции потоков. Параметры соединения задает источник, а его
-// временем жизни управляет тот, кто источник создал.
+// источника делят соединения, транзакции потоков и наблюдателей. Параметры соединения задает
+// источник, а его временем жизни управляет тот, кто источник создал.
//
// Параметры:
// ИсточникДанных - ИсточникДанных - Источник данных, из которого создается менеджер.
//
// Пример:
// Источник = Новый ИсточникДанных("Основной", Тип("КоннекторSQLite"), СтрокаСоединения);
+// Источник.ДобавитьНаблюдателя(Новый МойНаблюдатель);
// МенеджерСущностей = Новый МенеджерСущностей(Источник);
//
Процедура ПриСозданииОбъекта(Знач ИсточникДанных)
diff --git "a/docs/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.md" "b/docs/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.md"
new file mode 100644
index 00000000..7a8e2c4f
--- /dev/null
+++ "b/docs/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.md"
@@ -0,0 +1,54 @@
+## НаблюдаемыйКоннектор
+
+Интерфейс коннектора, умеющего сообщать наблюдателям источника данных о запросах к СУБД.
+
+Дополняет [АбстрактныйКоннектор](АбстрактныйКоннектор.md), а не заменяет его: коннектор без
+этого интерфейса работает как раньше, только наблюдатели не получают событий уровня запроса и
+описания соединения. Встроенные коннекторы объявляют оба интерфейса аннотацией `&Реализует`
+на конструкторе.
+
+Пул соединений после открытия коннектора передает ему реестр наблюдателей и запрашивает
+описание соединения. Описание никогда не содержит секретов: пароль из строки соединения
+в него не попадает.
+
+Подробнее - в разделе [Наблюдатели источника данных](Наблюдатели.md).
+
+### ПриСозданииОбъекта
+
+```bsl
+// Пример:
+// &Реализует("АбстрактныйКоннектор")
+// &Реализует("НаблюдаемыйКоннектор")
+// Процедура ПриСозданииОбъекта()
+// КонецПроцедуры
+//
+&Интерфейс
+Процедура ПриСозданииОбъекта()
+```
+
+### УстановитьНаблюдателей
+
+```bsl
+// Передает коннектору реестр наблюдателей, которым он сообщает о запросах.
+//
+// Параметры:
+// Наблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей пула. Коннектор создает события через
+// НачатьЗапрос и завершает через ЗавершитьЗапрос; при пустом реестре события не создаются.
+//
+Процедура УстановитьНаблюдателей(Наблюдатели) Экспорт
+```
+
+### ОписаниеСоединения
+
+```bsl
+// Описывает соединение для атрибутов событий.
+//
+// Возвращаемое значение:
+// Структура - Описание соединения без секретов:
+// * Система - Строка - СУБД: postgresql, sqlite, inmemory, json.
+// * Адрес - Строка - Адрес сервера; пустая строка, если сервера нет.
+// * Порт - Число - Порт сервера; 0, если сервера нет.
+// * БазаДанных - Строка - Имя базы, путь к файлу или каталогу.
+//
+Функция ОписаниеСоединения() Экспорт
+```
diff --git "a/docs/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270.md" "b/docs/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270.md"
new file mode 100644
index 00000000..23ba8780
--- /dev/null
+++ "b/docs/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270.md"
@@ -0,0 +1,208 @@
+## Наблюдатели источника данных
+
+### Зачем
+
+Наблюдатель - способ узнать, что происходит с источником данных, не меняя ни менеджер сущностей,
+ни коннектор: сколько длится операция, какой запрос ушел в СУБД и сколько строк вернул, сколько
+поток ждал соединение, чем закончилась транзакция. На этом строятся телеметрия (трейсы и метрики),
+журнал медленных запросов, подсчет обращений к БД в тестах.
+
+Наблюдатели регистрируются на [источнике данных](ИсточникДанных.md), а менеджер создается из
+источника. Наблюдатель видит все, что происходит с источником: операции менеджера, его хранилищ
+сущностей и активной записи, запросы, соединения и транзакции.
+
+### Наблюдатель
+
+Наблюдатель - объект с аннотацией `&Реализует("НаблюдательИсточникаДанных")` на конструкторе и двумя
+методами: `ПередСобытием` и `ПослеСобытия`. Оба получают один и тот же объект
+[СобытиеИсточникаДанных](СобытиеИсточникаДанных.md): до выполнения в нем заполнены описательные поля, после -
+длительность, ошибка и результат.
+
+```bsl
+// Класс МедленныеЗапросы
+
+&Реализует("НаблюдательИсточникаДанных")
+Процедура ПриСозданииОбъекта()
+КонецПроцедуры
+
+Процедура ПередСобытием(Событие) Экспорт
+КонецПроцедуры
+
+Процедура ПослеСобытия(Событие) Экспорт
+ Если Событие.Вид() = ВидыСобытийИсточникаДанных.Запрос() И Событие.Длительность() > 100 Тогда
+ Сообщить(СтрШаблон("Медленный %1 к %2: %3 мс", Событие.Операция(), Событие.ИмяТаблицы(), Событие.Длительность()));
+ КонецЕсли;
+КонецПроцедуры
+```
+
+Регистрация - методом `ДобавитьНаблюдателя` источника данных, снятие - `УдалитьНаблюдателя`.
+Реестр у источника и созданных из него менеджеров общий, поэтому регистрировать можно в любой
+момент, но наблюдатель, добавленный до `Инициализировать`, увидит и саму инициализацию.
+Регистрация проверяет реализацию через библиотеку extends: объект без аннотации отвергается с
+сообщением, какой аннотации не хватает, а объявивший интерфейс, но не реализовавший его целиком -
+сообщением валидатора о нереализованном интерфейсе.
+
+```bsl
+Источник = Новый ИсточникДанных("Основной", Тип("КоннекторSQLite"), СтрокаСоединения);
+Источник.ДобавитьНаблюдателя(Новый МедленныеЗапросы);
+
+МенеджерСущностей = Новый МенеджерСущностей(Источник);
+МенеджерСущностей.ДобавитьКлассВМодель(Тип("Автор"));
+МенеджерСущностей.Инициализировать();
+```
+
+Наблюдателей может быть несколько, они вызываются в порядке регистрации. Реестр общий на
+источник данных: у менеджеров и хранилищ сущностей своего нет. Состав получателей фиксируется при начале
+события: наблюдатель, снятый между фазами, все равно получает `ПослеСобытия` начатого при нем
+события, а добавленный между фазами видит только события, начатые после регистрации.
+
+### События
+
+У события есть вид, операция и две фазы. Виды - значения перечисления
+[ВидыСобытийИсточникаДанных](ВидыСобытийИсточникаДанных.md), с ними наблюдатель и сравнивает `Вид()`.
+Что заполнено в какой фазе:
+
+| Вид | Операция | До выполнения | После выполнения |
+| --- | --- | --- | --- |
+| Операция | Сохранить, Получить, ПолучитьОдно, Удалить, Инициализировать, ВыполнитьСКоннектором, ВычислитьСКоннектором | ТипСущности, ИмяТаблицы, Глубина | КоличествоСтрок у чтения |
+| Запрос | SELECT, INSERT, DELETE, CREATE TABLE, BEGIN, COMMIT, ROLLBACK | ИмяТаблицы, ТекстЗапроса | КоличествоСтрок у SELECT |
+| Соединение | Занять | - | Ожидание, Открыто, ВремяОткрытия; снимок пула: Занято, Свободно, Ожидает, Предел |
+| Соединение | Освободить | - | Снимок пула: Занято, Свободно, Ожидает, Предел |
+| Транзакция | Транзакция | - | Исход: commit, rollback, failed, abandoned |
+
+В любом событии заполнены `ТипКоннектора`, `Система`, `Адрес`, `Порт`, `БазаДанных` и `Начало`,
+а после выполнения - `Длительность` и `Ошибка`. Описание соединения никогда не содержит пароля:
+коннектор отдает его отдельной структурой, а не строкой соединения. Коннектор отдает описание
+после открытия, поэтому у событий, начатых до открытия первого соединения (инициализация и ее
+захват соединения), `Адрес`, `Порт` и `БазаДанных` в фазе `ПередСобытием` еще пусты и заполняются
+к фазе `ПослеСобытия`.
+
+**Операция** - вызов менеджера или хранилища из прикладного кода. Операция, порожденная другой
+операцией (разыменование ссылки на сущность при чтении, чтение подчиненных таблиц), приходит с
+`Глубина` на единицу больше. Наблюдатель, которому нужны только прикладные вызовы, отбирает
+события с нулевой глубиной; наблюдатель, строящий дерево спанов, вкладывает их по глубине.
+
+**Запрос** - обращение коннектора к СУБД. Его отдают коннекторы, реализующие
+[НаблюдаемыйКоннектор](НаблюдаемыйКоннектор.md); встроенные коннекторы реализуют его все.
+`ТекстЗапроса` содержит SQL с плейсхолдерами, а не значениями параметров; у коннекторов без SQL
+(InMemory, JSON) он `Неопределено`.
+
+**Соединение** - работа пула. `Занять` сообщает, досталось ли потоку свободное соединение или
+пришлось открыть новое (`Открыто`, `ВремяОткрытия`) и сколько поток простоял в очереди
+(`Ожидание`). Началом обоих событий отмечен вход в пул, поэтому их `Длительность` - полное время
+работы с пулом, включая ожидание его внутренней блокировки: у `Занять` - до выдачи соединения
+потоку, у `Освободить` - до возврата соединения, вместе с закрытием непригодного. Сама рассылка
+идет позже начала: пока пул не отработал, неизвестно, состоялся ли захват или возврат, а звать
+наблюдателя из-под блокировки пула нельзя. Оба события несут снимок пула после себя - по нему
+считаются метрики занятости.
+Повторный захват тем же потоком - операция внутри транзакции или вложенная операция - событий не
+порождает: соединение у потока уже есть. А вот соединение, которое поток не освободил сам, дает
+`Освободить` тогда, когда его возвращает в пул завершение этого потока. Неудачный возврат события
+не отменяет: соединение к тому моменту уже отобрано у потока, поэтому `Освободить` приходит с
+ошибкой в `Ошибка` - обычно это ошибка закрытия непригодного соединения.
+
+**Транзакция** - от `НачатьТранзакцию` до исхода; событие начинается до `BEGIN`, поэтому запрос
+`BEGIN` вложен в него. `commit` и `rollback` - штатное завершение; `failed` - начало, фиксация или
+отмена завершились ошибкой, она в `Ошибка`; `abandoned` - поток
+исполнения завершился, не закрыв транзакцию, и пул откатил ее при возврате соединения.
+
+### Ошибки
+
+Ошибка операции или запроса попадает в `Ошибка` события, а затем уходит вызывающему как обычно:
+наблюдатель ее видит, но не перехватывает.
+
+Ошибка самого наблюдателя операцию не прерывает: она пишется предупреждением в лог
+`oscript.lib.entity.observers`, и работа продолжается. Наблюдение - вспомогательная функция, и падать из-за
+телеметрии прикладной код не должен.
+
+### Потоки исполнения
+
+Наблюдателя никогда не зовут, пока пул держит свою внутреннюю блокировку: события, случившиеся
+под ней - брошенная транзакция при возврате соединения и ее откат, - рассылаются сразу после
+того, как блокировка снята. Поэтому медленный наблюдатель задерживает только свой поток, а не
+работу пула у всех остальных.
+
+У такого события обе фазы приходят задним числом, одна за другой: событие к моменту рассылки уже
+завершено, поэтому в `ПередСобытием` у него заполнены и `Длительность`, и `Ошибка`. Настенные часы
+наблюдателя тут ничего не измерят - спан длиной в ноль вместо настоящего отката. Границы такого
+спана строятся по самому событию, из `Начало()` и `Длительность()`; OpenTelemetry выставить их явно
+позволяет. Так приходит откат брошенной транзакции - единственный запрос, который пул делает под
+своей блокировкой.
+
+Пулом наблюдатель не управляет: события его не отдают, и звать `Занять` или `Освободить` у пула
+источника обработчику нельзя. Захват считается по парам, и лишнее освобождение вернет в пул
+соединение, которым занятая им операция еще пользуется, - его тут же получит другой поток. Своя
+работа с БД из обработчика этому не мешает, пока идет через менеджер: вложенный захват
+реентерабельный и глубину до нуля не опускает.
+
+Наблюдатель вызывается из всех потоков исполнения, работающих с менеджером, одновременно. Общее
+состояние наблюдателя должно быть синхронизировано, а то, что нужно передать от `ПередСобытием`
+к `ПослеСобытия` - открытый спан, момент начала, - кладется в само событие:
+
+```bsl
+Процедура ПередСобытием(Событие) Экспорт
+ Событие.УстановитьСостояние(Трассировщик.НачатьСпан(Событие.Операция()));
+КонецПроцедуры
+
+Процедура ПослеСобытия(Событие) Экспорт
+ Спан = Событие.Состояние();
+ Если Событие.Ошибка() <> Неопределено Тогда
+ Спан.ЗаписатьИсключение(Событие.Ошибка());
+ КонецЕсли;
+ Спан.Завершить();
+КонецПроцедуры
+```
+
+Событие принадлежит одному потоку и между потоками не передается, поэтому состояние в нем
+синхронизации не требует. Состояние у каждого наблюдателя свое: событие знает, кому оно
+рассылается сейчас, и `Состояние()` возвращает то, что положил именно этот наблюдатель.
+Несколько наблюдателей на одном менеджере друг другу не мешают.
+
+### Свой коннектор
+
+Коннектор, написанный вне библиотеки, наблюдателей не замечает и работает как раньше: события
+операций, соединений и транзакций приходят и с ним, только без запросов, а описание соединения
+сводится к имени типа коннектора в нижнем регистре в поле `Система`.
+
+Чтобы отдавать и запросы, коннектор объявляет второй интерфейс и реализует два метода:
+
+```bsl
+&Реализует("АбстрактныйКоннектор")
+&Реализует("НаблюдаемыйКоннектор")
+Процедура ПриСозданииОбъекта()
+КонецПроцедуры
+
+Процедура УстановитьНаблюдателей(Наблюдатели) Экспорт
+ РеестрНаблюдателей = Наблюдатели;
+КонецПроцедуры
+
+Функция ОписаниеСоединения() Экспорт
+ Возврат Новый Структура("Система, Адрес, Порт, БазаДанных", "mydb", "db.local", 1234, "test");
+КонецФункции
+```
+
+Реестр коннектор получает сразу после открытия. Событие запроса коннектор начинает и
+завершает сам, обернув обращение к СУБД:
+
+```bsl
+Событие = РеестрНаблюдателей.НачатьЗапрос("SELECT", ИмяТаблицы, ТекстЗапроса);
+Попытка
+ Строки = ВыполнитьВСУБД(ТекстЗапроса);
+Исключение
+ РеестрНаблюдателей.ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
+ ВызватьИсключение;
+КонецПопытки;
+РеестрНаблюдателей.ЗавершитьЗапрос(Событие, , Строки.Количество());
+```
+
+Без зарегистрированных наблюдателей `НачатьЗапрос` возвращает `Неопределено`, а `ЗавершитьЗапрос`
+с ним ничего не делает: коннектору не нужно проверять, есть ли кому слушать. Описание соединения
+не должно содержать пароля и других секретов.
+
+### Накладные расходы
+
+Без наблюдателей события не создаются: реестр только проверяет, что он пуст. С наблюдателями на
+каждую операцию приходится один объект события и два вызова каждого наблюдателя, плюс то же на
+каждый запрос, захват соединения и транзакцию. Стоимость наблюдения определяется самим
+наблюдателем; тяжелую работу - отправку телеметрии, запись в файл - ему следует выполнять
+асинхронно.
diff --git "a/docs/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.md" "b/docs/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.md"
new file mode 100644
index 00000000..61645be4
--- /dev/null
+++ "b/docs/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.md"
@@ -0,0 +1,65 @@
+## НаблюдательИсточникаДанных
+
+Интерфейс наблюдателя источника данных.
+
+Наблюдатель получает события источника данных, с которым работает менеджер сущностей:
+операции (Сохранить, Получить, ПолучитьОдно, Удалить, Инициализировать, ВыполнитьСКоннектором,
+ВычислитьСКоннектором), запросы к СУБД, захват и освобождение соединений, транзакции.
+Каждое событие приходит дважды: до выполнения и после, одним и тем же объектом
+[СобытиеИсточникаДанных](СобытиеИсточникаДанных.md).
+
+Реализация объявляет интерфейс аннотацией `&Реализует("НаблюдательИсточникаДанных")` на конструкторе
+и регистрируется на источнике данных методом `ДобавитьНаблюдателя`; менеджер, созданный из
+источника, отдает ей события. Регистрация проверяет реализацию через `ВалидаторРеализации`
+библиотеки extends, поэтому неполная реализация отвергается сразу.
+
+Наблюдатель вызывается из разных потоков исполнения одновременно и не должен держать
+несинхронизированного общего состояния: все, что нужно между фазами, кладется в событие
+методом `УстановитьСостояние`; у каждого наблюдателя это состояние свое. Исключение наблюдателя
+пишется в лог и операцию не прерывает.
+
+Подробнее - в разделе [Наблюдатели источника данных](Наблюдатели.md).
+
+### ПриСозданииОбъекта
+
+```bsl
+// Пример:
+// &Реализует("НаблюдательИсточникаДанных")
+// Процедура ПриСозданииОбъекта()
+// КонецПроцедуры
+//
+// Процедура ПередСобытием(Событие) Экспорт
+// Событие.УстановитьСостояние(ТекущаяУниверсальнаяДатаВМиллисекундах());
+// КонецПроцедуры
+//
+// Процедура ПослеСобытия(Событие) Экспорт
+// Сообщить(СтрШаблон("%1 %2: %3 мс", Событие.Вид(), Событие.Операция(), Событие.Длительность()));
+// КонецПроцедуры
+//
+&Интерфейс
+Процедура ПриСозданииОбъекта()
+```
+
+### ПередСобытием
+
+```bsl
+// Вызывается перед выполнением события.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных - Начатое событие. Поля результата обычно еще не заполнены;
+// у события, рассылка которого отложена, заполнены и они: см. Наблюдатели.md, раздел про
+// потоки исполнения.
+//
+Процедура ПередСобытием(Событие) Экспорт
+```
+
+### ПослеСобытия
+
+```bsl
+// Вызывается после выполнения события, в том числе завершившегося ошибкой.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных - Завершенное событие: заполнены Длительность, Ошибка и поля результата.
+//
+Процедура ПослеСобытия(Событие) Экспорт
+```
diff --git "a/docs/\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.md" "b/docs/\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.md"
new file mode 100644
index 00000000..8acadf4e
--- /dev/null
+++ "b/docs/\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.md"
@@ -0,0 +1,478 @@
+## СобытиеИсточникаДанных
+
+Событие источника данных, передаваемое наблюдателям.
+
+Один и тот же объект приходит наблюдателю дважды: в `ПередСобытием`, когда заполнены только
+описательные поля, и в `ПослеСобытия`, когда заполнены длительность, ошибка и поля результата.
+Поля читаются функциями; менять их наблюдателю не следует. Для своих данных между фазами
+у наблюдателя есть `Состояние` - у каждого наблюдателя свое, чужого он не видит.
+
+Виды событий - значения перечисления [ВидыСобытийИсточникаДанных](ВидыСобытийИсточникаДанных.md). Заполняемые поля:
+
+* Операция - операция менеджера или хранилища: Операция, ТипСущности, ИмяТаблицы, Глубина;
+ после - КоличествоСтрок для чтения.
+* Запрос - запрос к СУБД: Операция (SELECT, INSERT, DELETE, CREATE TABLE, BEGIN, COMMIT,
+ ROLLBACK), ИмяТаблицы, ТекстЗапроса (Неопределено у коннекторов без SQL); после - КоличествоСтрок.
+* Соединение - захват и освобождение соединения пула: Операция (Занять, Освободить); после -
+ снимок пула: Занято, Свободно, Ожидает, Предел. Ожидание, Открыто и ВремяОткрытия описывают
+ захват, поэтому заполнены только у Занять.
+* Транзакция - транзакция потока исполнения: после - Исход (commit, rollback, failed, abandoned).
+
+Методы с пометкой `@internal` заполняют событие изнутри библиотеки.
+
+Подробнее - в разделе [Наблюдатели источника данных](Наблюдатели.md).
+
+### ПриСозданииОбъекта
+
+```bsl
+// Создает начатое событие.
+//
+// Создается библиотекой, напрямую конструировать не следует.
+//
+// Параметры:
+// ПВид - Строка - Вид события, значение перечисления ВидыСобытийИсточникаДанных.
+// ПОперация - Строка - Операция события.
+// ПГлубина - Число - Вложенность операции.
+//
+Процедура ПриСозданииОбъекта(Знач ПВид, Знач ПОперация, Знач ПГлубина = 0)
+```
+
+### Вид
+
+```bsl
+// Вид события.
+//
+// Возвращаемое значение:
+// Строка - Значение перечисления ВидыСобытийИсточникаДанных: Операция, Запрос, Соединение или Транзакция.
+//
+Функция Вид() Экспорт
+```
+
+### Операция
+
+```bsl
+// Операция события.
+//
+// Возвращаемое значение:
+// Строка - Имя операции, см. описание класса.
+//
+Функция Операция() Экспорт
+```
+
+### Глубина
+
+```bsl
+// Вложенность операции.
+//
+// Возвращаемое значение:
+// Число - 0 у операции из прикладного кода, 1 у порожденной ею разыменованием ссылки и т.д.
+//
+Функция Глубина() Экспорт
+```
+
+### ТипСущности
+
+```bsl
+// Тип сущности операции.
+//
+// Возвращаемое значение:
+// Тип - Тип сущности.
+// Неопределено - Операция не привязана к сущности.
+//
+Функция ТипСущности() Экспорт
+```
+
+### ИмяТаблицы
+
+```bsl
+// Имя таблицы сущности или запроса.
+//
+// Возвращаемое значение:
+// Строка - Имя таблицы; пустая строка, если таблицы нет.
+//
+Функция ИмяТаблицы() Экспорт
+```
+
+### ТекстЗапроса
+
+```bsl
+// Текст запроса к СУБД.
+//
+// Возвращаемое значение:
+// Строка - Текст запроса с плейсхолдерами параметров.
+// Неопределено - Коннектор не формирует текст запроса.
+//
+Функция ТекстЗапроса() Экспорт
+```
+
+### КоличествоСтрок
+
+```bsl
+// Число строк результата чтения.
+//
+// Возвращаемое значение:
+// Число - Число строк.
+// Неопределено - Событие не читает строки или еще не завершено.
+//
+Функция КоличествоСтрок() Экспорт
+```
+
+### ТипКоннектора
+
+```bsl
+// Тип коннектора пула.
+//
+// Возвращаемое значение:
+// Тип - Тип класса коннектора.
+//
+Функция ТипКоннектора() Экспорт
+```
+
+### Система
+
+```bsl
+// Система управления базой данных.
+//
+// Возвращаемое значение:
+// Строка - postgresql, sqlite, inmemory, json или имя типа коннектора в нижнем регистре.
+//
+Функция Система() Экспорт
+```
+
+### Адрес
+
+```bsl
+// Адрес сервера СУБД.
+//
+// Возвращаемое значение:
+// Строка - Адрес; пустая строка, если у СУБД нет сервера.
+//
+Функция Адрес() Экспорт
+```
+
+### Порт
+
+```bsl
+// Порт сервера СУБД.
+//
+// Возвращаемое значение:
+// Число - Порт; 0, если у СУБД нет сервера.
+//
+Функция Порт() Экспорт
+```
+
+### БазаДанных
+
+```bsl
+// Имя базы данных.
+//
+// Возвращаемое значение:
+// Строка - Имя базы, путь к файлу или строка подключения без секретов.
+//
+Функция БазаДанных() Экспорт
+```
+
+### Начало
+
+```bsl
+// Момент начала события.
+//
+// Может быть раньше рассылки ПередСобытием: событие захвата соединения начинается со входа
+// в пул, а разослано будет, когда станет известно, что событие вообще нужно.
+//
+// Возвращаемое значение:
+// Число - Универсальная дата в миллисекундах.
+//
+Функция Начало() Экспорт
+```
+
+### Длительность
+
+```bsl
+// Длительность события.
+//
+// У события, рассылка которого отложена, заполнена уже в ПередСобытием: такое событие приходит
+// наблюдателю задним числом, обеими фазами подряд. См. Наблюдатели.md, "Потоки исполнения".
+//
+// Возвращаемое значение:
+// Число - Миллисекунды от начала до завершения.
+// Неопределено - Событие еще не завершено.
+//
+Функция Длительность() Экспорт
+```
+
+### Ошибка
+
+```bsl
+// Ошибка, которой завершилось событие.
+//
+// Возвращаемое значение:
+// ИнформацияОбОшибке - Ошибка.
+// Неопределено - Событие завершилось успешно или еще не завершено.
+//
+Функция Ошибка() Экспорт
+```
+
+### Состояние
+
+```bsl
+// Данные наблюдателя, сохраненные между фазами.
+//
+// У каждого наблюдателя свое состояние: возвращается то, что в ПередСобытием положил
+// наблюдатель, которому событие рассылается сейчас.
+//
+// Возвращаемое значение:
+// Произвольный - Что положил наблюдатель; Неопределено, если ничего.
+//
+Функция Состояние() Экспорт
+```
+
+### УстановитьСостояние
+
+```bsl
+// Сохраняет данные наблюдателя до фазы ПослеСобытия.
+//
+// Состояние принадлежит наблюдателю, который его положил: другие наблюдатели того же
+// события его не видят и не затирают.
+//
+// Параметры:
+// Значение - Произвольный - Данные наблюдателя, например открытый спан.
+//
+Процедура УстановитьСостояние(Значение) Экспорт
+```
+
+### УстановитьТекущегоНаблюдателя
+
+```bsl
+// @internal
+// Отмечает наблюдателя, которому событие рассылается сейчас: Состояние читается и пишется для него.
+//
+// Параметры:
+// Наблюдатель - НаблюдательИсточникаДанных, Неопределено - Наблюдатель; Неопределено - рассылка окончена.
+//
+Процедура УстановитьТекущегоНаблюдателя(Знач Наблюдатель) Экспорт
+```
+
+### Получатели
+
+```bsl
+// @internal
+// Получатели события: наблюдатели, зарегистрированные на момент его создания.
+//
+// Возвращаемое значение:
+// ФиксированныйМассив из НаблюдательИсточникаДанных - Кому событие рассылается в обеих фазах.
+//
+Функция Получатели() Экспорт
+```
+
+### УстановитьПолучателей
+
+```bsl
+// @internal
+// Фиксирует получателей события: обе фазы уходят одному и тому же снимку реестра.
+//
+// Параметры:
+// ПНаблюдатели - ФиксированныйМассив из НаблюдательИсточникаДанных - Снимок реестра.
+//
+Процедура УстановитьПолучателей(Знач ПНаблюдатели) Экспорт
+```
+
+### Исход
+
+```bsl
+// Исход транзакции.
+//
+// Возвращаемое значение:
+// Строка - commit, rollback, failed или abandoned.
+// Неопределено - Событие не о транзакции или еще не завершено.
+//
+Функция Исход() Экспорт
+```
+
+### Ожидание
+
+```bsl
+// Время ожидания соединения в очереди пула.
+//
+// Возвращаемое значение:
+// Число - Миллисекунды; 0, если соединение досталось сразу.
+// Неопределено - Событие не о захвате соединения.
+//
+Функция Ожидание() Экспорт
+```
+
+### Открыто
+
+```bsl
+// Признак открытия нового соединения при захвате.
+//
+// Возвращаемое значение:
+// Булево - Истина, если пул открыл новое соединение, а не выдал свободное.
+// Неопределено - Событие не о захвате соединения.
+//
+Функция Открыто() Экспорт
+```
+
+### ВремяОткрытия
+
+```bsl
+// Время открытия нового соединения.
+//
+// Возвращаемое значение:
+// Число - Миллисекунды; 0, если соединение не открывалось.
+// Неопределено - Событие не о захвате соединения.
+//
+Функция ВремяОткрытия() Экспорт
+```
+
+### Занято
+
+```bsl
+// Соединений, занятых потоками исполнения, после события.
+//
+// Возвращаемое значение:
+// Число - Занятые соединения.
+// Неопределено - Событие не о соединении.
+//
+Функция Занято() Экспорт
+```
+
+### Свободно
+
+```bsl
+// Свободных соединений в пуле после события.
+//
+// Возвращаемое значение:
+// Число - Свободные соединения.
+// Неопределено - Событие не о соединении.
+//
+Функция Свободно() Экспорт
+```
+
+### Ожидает
+
+```bsl
+// Потоков исполнения в очереди за соединением после события.
+//
+// Возвращаемое значение:
+// Число - Ожидающие.
+// Неопределено - Событие не о соединении.
+//
+Функция Ожидает() Экспорт
+```
+
+### Предел
+
+```bsl
+// Размер пула.
+//
+// Возвращаемое значение:
+// Число - Предел одновременно живущих соединений; 0 - без предела.
+// Неопределено - Событие не о соединении.
+//
+Функция Предел() Экспорт
+```
+
+### УстановитьСущность
+
+```bsl
+// @internal
+// Привязывает событие к сущности.
+//
+// Параметры:
+// ПТипСущности - Тип - Тип сущности.
+// ПИмяТаблицы - Строка - Имя таблицы.
+//
+Процедура УстановитьСущность(Знач ПТипСущности, Знач ПИмяТаблицы) Экспорт
+```
+
+### УстановитьЗапрос
+
+```bsl
+// @internal
+// Заполняет запрос к СУБД.
+//
+// Параметры:
+// ПИмяТаблицы - Строка - Имя таблицы запроса.
+// ПТекстЗапроса - Строка, Неопределено - Текст запроса.
+//
+Процедура УстановитьЗапрос(Знач ПИмяТаблицы, Знач ПТекстЗапроса) Экспорт
+```
+
+### УстановитьКоннектор
+
+```bsl
+// @internal
+// Заполняет сведения о коннекторе и соединении.
+//
+// Параметры:
+// ПТипКоннектора - Тип - Тип коннектора.
+// Описание - Структура - Система, Адрес, Порт, БазаДанных.
+//
+Процедура УстановитьКоннектор(Знач ПТипКоннектора, Знач Описание) Экспорт
+```
+
+### УстановитьКоличествоСтрок
+
+```bsl
+// @internal
+// Заполняет число строк результата.
+//
+// Параметры:
+// ПКоличествоСтрок - Число - Число строк.
+//
+Процедура УстановитьКоличествоСтрок(Знач ПКоличествоСтрок) Экспорт
+```
+
+### УстановитьИсход
+
+```bsl
+// @internal
+// Заполняет исход транзакции.
+//
+// Параметры:
+// ПИсход - Строка - commit, rollback, failed или abandoned.
+//
+Процедура УстановитьИсход(Знач ПИсход) Экспорт
+```
+
+### УстановитьДанныеЗахвата
+
+```bsl
+// @internal
+// Заполняет сведения о захвате соединения.
+//
+// Параметры:
+// ПОжидание - Число - Ожидание в очереди, мс.
+// ПОткрыто - Булево - Открыто ли новое соединение.
+// ПВремяОткрытия - Число - Время открытия, мс.
+//
+Процедура УстановитьДанныеЗахвата(Знач ПОжидание, Знач ПОткрыто, Знач ПВремяОткрытия) Экспорт
+```
+
+### УстановитьСнимокПула
+
+```bsl
+// @internal
+// Заполняет снимок пула после события.
+//
+// Параметры:
+// ПЗанято - Число - Занятые соединения.
+// ПСвободно - Число - Свободные соединения.
+// ПОжидает - Число - Ожидающие потоки исполнения.
+// ППредел - Число - Размер пула.
+//
+Процедура УстановитьСнимокПула(Знач ПЗанято, Знач ПСвободно, Знач ПОжидает, Знач ППредел) Экспорт
+```
+
+### Завершить
+
+```bsl
+// @internal
+// Завершает событие: фиксирует длительность и ошибку.
+//
+// Параметры:
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка, если событие ею завершилось.
+//
+Процедура Завершить(Знач ИнформацияОбОшибке = Неопределено) Экспорт
+```
diff --git a/lib.config b/lib.config
index 5972012a..80b339ac 100644
--- a/lib.config
+++ b/lib.config
@@ -13,6 +13,9 @@
+
+
+
@@ -21,4 +24,5 @@
+
\ No newline at end of file
diff --git a/packagedef b/packagedef
index f59fecee..c2e7c0c3 100644
--- a/packagedef
+++ b/packagedef
@@ -24,6 +24,7 @@
.ЗависитОт("lambdas", "0.3.4")
.ЗависитОт("logos", "1.7.1")
.ЗависитОт("decorator", "3.0.0")
+ .ЗависитОт("extends", "0.2.0")
.ЗависитОт("reflector", "0.7.1")
.ЗависитОт("sql", "1.3.3")
.ЗависитОт("strings", "0.5.0")
diff --git "a/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os" "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
new file mode 100644
index 00000000..37dadc34
--- /dev/null
+++ "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
@@ -0,0 +1,502 @@
+#Использовать "../concurrent"
+
+#Использовать annotations
+#Использовать reflector
+#Использовать extends
+#Использовать logos
+
+// ФиксированныйМассив - Зарегистрированные наблюдатели. Меняется целиком: событие при создании
+// берет текущий снимок и рассылается ему в обеих фазах, какие бы регистрации ни случились между ними.
+Перем Наблюдатели;
+// БлокировкаЧтенияЗаписи - Защищает замену снимка при одновременной регистрации из разных потоков.
+Перем БлокировкаРеестра;
+// Тип - Тип коннектора пула.
+Перем ТипКоннектора;
+// Структура - Система, Адрес, Порт, БазаДанных: чем и куда подключается пул.
+Перем ОписаниеСоединения;
+// Строка - Ключ вложенности операций в данных потока исполнения.
+Перем КлючГлубины;
+// Строка - Ключ отложенных рассылок в данных потока исполнения.
+Перем КлючОтложенных;
+// Лог - Журнал наблюдателей.
+Перем Лог;
+// ВалидаторРеализации - Проверяет реализацию интерфейса при регистрации.
+Перем Валидатор;
+// ОпределениеАннотации - Определение аннотации &Реализует для чтения имени интерфейса.
+Перем ОпределениеАннотацииРеализует;
+// Булево - Объявляет ли коннектор пула интерфейс НаблюдаемыйКоннектор. Неопределено - еще не проверялось.
+Перем КоннекторНаблюдаем;
+
+// Реестр наблюдателей источника данных.
+//
+// Один на источник данных: пул соединений общий у менеджера и его хранилищ, поэтому события
+// всех операций, запросов, соединений и транзакций идут через один реестр. При пустом реестре
+// события не создаются: НачатьОперацию возвращает Неопределено, а Завершить с ним ничего не
+// делает.
+//
+// Исключение наблюдателя пишется в лог и не прерывает операцию: наблюдение не должно ломать
+// то, за чем наблюдает.
+//
+// Параметры:
+// ПТипКоннектора - Тип - Тип коннектора пула.
+//
+Процедура ПриСозданииОбъекта(Знач ПТипКоннектора)
+
+ Наблюдатели = Новый ФиксированныйМассив(Новый Массив);
+ БлокировкаРеестра = Новый БлокировкаЧтенияЗаписи;
+ ТипКоннектора = ПТипКоннектора;
+ ОписаниеСоединения = СтрокиСоединения.ОписаниеБезСервера(СистемаПоТипу(ПТипКоннектора), "");
+ КлючГлубины = "entity.observers." + Строка(Новый УникальныйИдентификатор);
+ КлючОтложенных = "entity.observers.deferred." + Строка(Новый УникальныйИдентификатор);
+ Лог = Логирование.ПолучитьЛог("oscript.lib.entity.observers");
+
+ КонтейнерАннотаций = Новый КонтейнерАннотаций();
+ КонтейнерАннотаций.ДобавитьАннотацию(Тип("АннотацияРеализует"));
+ КонтейнерАннотаций.ДобавитьАннотацию(Тип("АннотацияИнтерфейс"));
+ КонтейнерАннотаций.ДобавитьАннотацию(Тип("АннотацияРасширяет"));
+ ОпределениеАннотацииРеализует = КонтейнерАннотаций.ПолучитьОпределениеАннотации("Реализует");
+ Валидатор = Новый ВалидаторРеализации(КонтейнерАннотаций);
+
+КонецПроцедуры
+
+// Регистрирует наблюдателя.
+//
+// Наблюдатель должен объявить интерфейс аннотацией &Реализует("НаблюдательИсточникаДанных") и
+// реализовать его целиком; иначе регистрация завершается ошибкой. Повторная регистрация того
+// же объекта ничего не меняет.
+//
+// Параметры:
+// Наблюдатель - Произвольный - Объект, реализующий НаблюдательИсточникаДанных.
+//
+Процедура Добавить(Наблюдатель) Экспорт
+
+ ПроверитьОбъявлениеИнтерфейса(Наблюдатель);
+ Валидатор.ИнтерфейсыРеализованыКорректно(Наблюдатель);
+
+ // Замена снимка - чтение-изменение-запись: без блокировки две одновременные регистрации
+ // построили бы новый снимок из одного старого, и одна из них потерялась бы.
+ БлокировкаРеестра.Запись().Заблокировать();
+
+ Попытка
+
+ Если Наблюдатели.Найти(Наблюдатель) = Неопределено Тогда
+ Новые = Новый Массив(Наблюдатели);
+ Новые.Добавить(Наблюдатель);
+ Наблюдатели = Новый ФиксированныйМассив(Новые);
+ КонецЕсли;
+
+ Исключение
+ БлокировкаРеестра.Запись().Разблокировать();
+ ВызватьИсключение;
+ КонецПопытки;
+
+ БлокировкаРеестра.Запись().Разблокировать();
+
+КонецПроцедуры
+
+// Снимает наблюдателя с регистрации.
+//
+// Параметры:
+// Наблюдатель - Произвольный - Ранее зарегистрированный наблюдатель. Незарегистрированный пропускается.
+//
+Процедура Удалить(Наблюдатель) Экспорт
+
+ БлокировкаРеестра.Запись().Заблокировать();
+
+ Попытка
+
+ Индекс = Наблюдатели.Найти(Наблюдатель);
+ Если Индекс <> Неопределено Тогда
+ Новые = Новый Массив(Наблюдатели);
+ Новые.Удалить(Индекс);
+ Наблюдатели = Новый ФиксированныйМассив(Новые);
+ КонецЕсли;
+
+ Исключение
+ БлокировкаРеестра.Запись().Разблокировать();
+ ВызватьИсключение;
+ КонецПопытки;
+
+ БлокировкаРеестра.Запись().Разблокировать();
+
+КонецПроцедуры
+
+// Определяет, зарегистрирован ли хоть один наблюдатель.
+//
+// Возвращаемое значение:
+// Булево - Истина, если наблюдателей нет.
+//
+Функция Пусто() Экспорт
+ Возврат Наблюдатели.Количество() = 0;
+КонецФункции
+
+// Число зарегистрированных наблюдателей.
+//
+// Возвращаемое значение:
+// Число - Количество.
+//
+Функция Количество() Экспорт
+ Возврат Наблюдатели.Количество();
+КонецФункции
+
+// Описание соединения, которым помечаются события.
+//
+// Возвращаемое значение:
+// Структура - Система, Адрес, Порт, БазаДанных.
+//
+Функция ОписаниеСоединения() Экспорт
+ Возврат ОписаниеСоединения;
+КонецФункции
+
+// Запоминает описание соединения, полученное от коннектора.
+//
+// Параметры:
+// Описание - Структура - Система, Адрес, Порт, БазаДанных.
+//
+Процедура УстановитьОписаниеСоединения(Знач Описание) Экспорт
+ ОписаниеСоединения = Описание;
+КонецПроцедуры
+
+// Подключает к реестру только что открытый коннектор пула.
+//
+// Коннектор, объявивший интерфейс НаблюдаемыйКоннектор, получает реестр и сообщает описание
+// соединения; остальные работают без событий уровня запроса. Объявление проверяется один раз
+// на тип коннектора, реализация - валидатором extends.
+//
+// Ответ запоминается только после успешной проверки: иначе коннектор, объявивший интерфейс и не
+// реализовавший его, прошел бы валидатор единожды, а следующие открытия соединения жаловались бы
+// на ненайденный метод вместо интерфейса.
+//
+// Параметры:
+// Коннектор - АбстрактныйКоннектор - Открытый коннектор.
+//
+Процедура ПодключитьКоннектор(Коннектор) Экспорт
+
+ Если КоннекторНаблюдаем = Неопределено Тогда
+ Наблюдаем = ОбъявляетИнтерфейс(Коннектор, "НаблюдаемыйКоннектор");
+ Если Наблюдаем Тогда
+ Валидатор.ИнтерфейсыРеализованыКорректно(Коннектор);
+ КонецЕсли;
+ КоннекторНаблюдаем = Наблюдаем;
+ КонецЕсли;
+
+ Если НЕ КоннекторНаблюдаем Тогда
+ Возврат;
+ КонецЕсли;
+
+ Коннектор.УстановитьНаблюдателей(ЭтотОбъект);
+ ОписаниеСоединения = Коннектор.ОписаниеСоединения();
+
+КонецПроцедуры
+
+// Начинает событие запроса к СУБД и рассылает его наблюдателям.
+//
+// Параметры:
+// ОперацияСУБД - Строка - SELECT, INSERT, DELETE, CREATE TABLE, BEGIN, COMMIT, ROLLBACK.
+// ИмяТаблицы - Строка - Таблица запроса; пустая строка, если таблицы нет.
+// ТекстЗапроса - Строка, Неопределено - Текст запроса; Неопределено у коннекторов без SQL.
+//
+// Возвращаемое значение:
+// СобытиеИсточникаДанных - Начатое событие.
+// Неопределено - Наблюдателей нет, событие не создавалось.
+//
+Функция НачатьЗапрос(Знач ОперацияСУБД, Знач ИмяТаблицы = "", Знач ТекстЗапроса = Неопределено) Экспорт
+
+ Событие = СоздатьСобытие(ВидыСобытийИсточникаДанных.Запрос(), ОперацияСУБД);
+ Если Событие = Неопределено Тогда
+ Возврат Неопределено;
+ КонецЕсли;
+
+ Событие.УстановитьЗапрос(ИмяТаблицы, ТекстЗапроса);
+ Разослать("ПередСобытием", Событие);
+
+ Возврат Событие;
+
+КонецФункции
+
+// Завершает событие запроса и рассылает его наблюдателям.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных, Неопределено - Событие из НачатьЗапрос. С Неопределено ничего не делает.
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка, которой завершился запрос.
+// КоличествоСтрок - Число, Неопределено - Число строк результата.
+//
+Процедура ЗавершитьЗапрос(Событие, Знач ИнформацияОбОшибке = Неопределено, Знач КоличествоСтрок = Неопределено) Экспорт
+
+ Если Событие = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ Если КоличествоСтрок <> Неопределено Тогда
+ Событие.УстановитьКоличествоСтрок(КоличествоСтрок);
+ КонецЕсли;
+
+ ЗавершитьСобытие(Событие, ИнформацияОбОшибке);
+
+КонецПроцедуры
+
+// Начинает событие операции и рассылает его наблюдателям.
+//
+// Параметры:
+// Операция - Строка - Имя операции.
+// ОбъектМодели - ОбъектМодели, Неопределено - Модель сущности операции.
+//
+// Возвращаемое значение:
+// СобытиеИсточникаДанных - Начатое событие.
+// Неопределено - Наблюдателей нет, событие не создавалось.
+//
+Функция НачатьОперацию(Знач Операция, Знач ОбъектМодели = Неопределено) Экспорт
+
+ // Нужно ли событие, решает одна проверка - внутри СоздатьСобытие. Спроси мы отдельно, есть ли
+ // наблюдатели, между вопросом и ответом последнего могли бы снять: событие не создалось бы,
+ // а глубина уже была бы увеличена, и снять ее было бы некому - ЗавершитьОперацию без события
+ // ничего не делает.
+ Событие = СоздатьСобытие(ВидыСобытийИсточникаДанных.Операция(), Операция, ТекущаяГлубина());
+ Если Событие = Неопределено Тогда
+ Возврат Неопределено;
+ КонецЕсли;
+
+ ИзменитьГлубину(1);
+
+ Если ОбъектМодели <> Неопределено Тогда
+ Событие.УстановитьСущность(ОбъектМодели.ТипСущности(), ОбъектМодели.ИмяТаблицы());
+ КонецЕсли;
+
+ Разослать("ПередСобытием", Событие);
+
+ Возврат Событие;
+
+КонецФункции
+
+// Завершает событие операции и рассылает его наблюдателям.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных, Неопределено - Событие из НачатьОперацию. С Неопределено ничего не делает.
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка, которой завершилась операция.
+// КоличествоСтрок - Число, Неопределено - Число прочитанных строк.
+//
+Процедура ЗавершитьОперацию(Событие, Знач ИнформацияОбОшибке = Неопределено,
+ Знач КоличествоСтрок = Неопределено) Экспорт
+
+ Если Событие = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ ИзменитьГлубину(-1);
+
+ Если КоличествоСтрок <> Неопределено Тогда
+ Событие.УстановитьКоличествоСтрок(КоличествоСтрок);
+ КонецЕсли;
+
+ ЗавершитьСобытие(Событие, ИнформацияОбОшибке);
+
+КонецПроцедуры
+
+// Создает событие любого вида, помеченное коннектором пула.
+//
+// Событие запоминает снимок наблюдателей на момент создания и обеим фазам рассылается ему:
+// наблюдатель, снятый между фазами, все равно получает завершение начатого при нем события,
+// а добавленный между фазами видит только события, начатые после регистрации.
+//
+// Параметры:
+// Вид - Строка - Вид события.
+// Операция - Строка - Операция события.
+// Глубина - Число - Вложенность.
+// НачалоСобытия - Число - Момент начала работы в миллисекундах, если она началась раньше, чем
+// стало известно, нужно ли событие вообще. Неопределено - началом считается создание события.
+//
+// Возвращаемое значение:
+// СобытиеИсточникаДанных - Событие, еще не разосланное.
+// Неопределено - Наблюдателей нет.
+//
+Функция СоздатьСобытие(Знач Вид, Знач Операция, Знач Глубина = 0, Знач НачалоСобытия = Неопределено) Экспорт
+
+ Если Пусто() Тогда
+ Возврат Неопределено;
+ КонецЕсли;
+
+ Событие = Новый СобытиеИсточникаДанных(Вид, Операция, Глубина, НачалоСобытия);
+ Событие.УстановитьКоннектор(ТипКоннектора, ОписаниеСоединения);
+ Событие.УстановитьПолучателей(Наблюдатели);
+
+ Возврат Событие;
+
+КонецФункции
+
+// Рассылает начало события.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных, Неопределено - Событие из СоздатьСобытие.
+//
+Процедура НачатьСобытие(Событие) Экспорт
+
+ Если Событие = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ Разослать("ПередСобытием", Событие);
+
+КонецПроцедуры
+
+// Завершает событие и рассылает его завершение.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных, Неопределено - Начатое событие.
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка, которой завершилось событие.
+//
+Процедура ЗавершитьСобытие(Событие, Знач ИнформацияОбОшибке = Неопределено) Экспорт
+
+ Если Событие = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ // Событие, начатое до открытия первого соединения (инициализация и ее захват), создавалось
+ // без описания от коннектора; к завершению описание уже известно.
+ Событие.УстановитьКоннектор(ТипКоннектора, ОписаниеСоединения);
+ Событие.Завершить(ИнформацияОбОшибке);
+ Разослать("ПослеСобытия", Событие);
+
+КонецПроцедуры
+
+// Откладывает рассылку событий текущего потока исполнения до РазослатьОтложенное.
+//
+// Нужно тому, кто держит блокировку: наблюдатель - чужой код, и звать его, не отпустив
+// блокировку, нельзя. События, случившиеся под ней, дождутся своей рассылки в порядке
+// возникновения, обе фазы каждого события в свой черед.
+//
+Процедура НачатьОтложеннуюРассылку() Экспорт
+
+ Если ТекущийПоток().Данные.Получить(КлючОтложенных) <> Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ ТекущийПоток().Данные.Вставить(КлючОтложенных, Новый Массив);
+
+КонецПроцедуры
+
+// Рассылает то, что накопилось с НачатьОтложеннуюРассылку, и выключает откладывание.
+//
+// Вызывается сразу после снятия блокировки. Без пары к НачатьОтложеннуюРассылку ничего не делает.
+//
+Процедура РазослатьОтложенное() Экспорт
+
+ Отложенные = ТекущийПоток().Данные.Получить(КлючОтложенных);
+ Если Отложенные = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ ТекущийПоток().Данные.Удалить(КлючОтложенных);
+
+ Для Каждого Рассылка Из Отложенные Цикл
+ Разослать(Рассылка.ИмяМетода, Рассылка.Событие);
+ КонецЦикла;
+
+КонецПроцедуры
+
+Процедура Разослать(Знач ИмяМетода, Событие)
+
+ Отложенные = ТекущийПоток().Данные.Получить(КлючОтложенных);
+ Если Отложенные <> Неопределено Тогда
+ Отложенные.Добавить(Новый Структура("ИмяМетода, Событие", ИмяМетода, Событие));
+ Возврат;
+ КонецЕсли;
+
+ Для Каждого Наблюдатель Из Событие.Получатели() Цикл
+
+ // Состояние события у каждого наблюдателя свое: событие должно знать, кому оно сейчас отдано.
+ Событие.УстановитьТекущегоНаблюдателя(Наблюдатель);
+ Попытка
+ Если ИмяМетода = "ПередСобытием" Тогда
+ Наблюдатель.ПередСобытием(Событие);
+ Иначе
+ Наблюдатель.ПослеСобытия(Событие);
+ КонецЕсли;
+ Исключение
+ Лог.Предупреждение(
+ "Наблюдатель %1 завершился ошибкой в %2 события %3 %4: %5",
+ ТипЗнч(Наблюдатель),
+ ИмяМетода,
+ Событие.Вид(),
+ Событие.Операция(),
+ ОписаниеОшибки()
+ );
+ КонецПопытки;
+
+ КонецЦикла;
+
+ Событие.УстановитьТекущегоНаблюдателя(Неопределено);
+
+КонецПроцедуры
+
+Функция ТекущаяГлубина()
+
+ Глубина = ТекущийПоток().Данные.Получить(КлючГлубины);
+
+ Возврат ?(Глубина = Неопределено, 0, Глубина);
+
+КонецФункции
+
+Процедура ИзменитьГлубину(Знач Приращение)
+ ТекущийПоток().Данные.Вставить(КлючГлубины, Макс(ТекущаяГлубина() + Приращение, 0));
+КонецПроцедуры
+
+// Определяет, объявляет ли объект интерфейс аннотацией &Реализует на любом своем методе.
+//
+// Параметры:
+// Объект - Произвольный - Проверяемый объект.
+// ИмяИнтерфейса - Строка - Имя интерфейса.
+//
+// Возвращаемое значение:
+// Булево - Истина, если объявление найдено.
+//
+Функция ОбъявляетИнтерфейс(Объект, Знач ИмяИнтерфейса)
+
+ Рефлектор = Новый РефлекторОбъекта(Объект);
+ МетодыСАннотацией = Рефлектор.ПолучитьТаблицуМетодов("Реализует", Ложь);
+
+ Для Каждого Метод Из МетодыСАннотацией Цикл
+ Аннотации = РаботаСАннотациями.НайтиАннотации(Метод.Аннотации, "Реализует");
+ Для Каждого Аннотация Из Аннотации Цикл
+ ОбъектАннотации = ОпределениеАннотацииРеализует.СоздатьОбъектАннотации(Аннотация);
+ Если ОбъектАннотации.ИмяИнтерфейса() = ИмяИнтерфейса Тогда
+ Возврат Истина;
+ КонецЕсли;
+ КонецЦикла;
+ КонецЦикла;
+
+ Возврат Ложь;
+
+КонецФункции
+
+Процедура ПроверитьОбъявлениеИнтерфейса(Наблюдатель)
+
+ Если ОбъявляетИнтерфейс(Наблюдатель, "НаблюдательИсточникаДанных") Тогда
+ Возврат;
+ КонецЕсли;
+
+ ТипНаблюдателя = ТипЗнч(Наблюдатель);
+ ВызватьИсключение СтрШаблон(
+ "Объект типа <%1> не объявляет интерфейс НаблюдательИсточникаДанных: "
+ + "нужна аннотация &Реализует(""НаблюдательИсточникаДанных"") на конструкторе",
+ ТипНаблюдателя
+ );
+
+КонецПроцедуры
+
+Функция СистемаПоТипу(Знач ПТипКоннектора)
+
+ ИмяТипа = Строка(ПТипКоннектора);
+
+ Если ИмяТипа = "КоннекторPostgreSQL" Тогда
+ Возврат "postgresql";
+ ИначеЕсли ИмяТипа = "КоннекторSQLite" Тогда
+ Возврат "sqlite";
+ ИначеЕсли ИмяТипа = "КоннекторInMemory" Тогда
+ Возврат "inmemory";
+ ИначеЕсли ИмяТипа = "КоннекторJSON" Тогда
+ Возврат "json";
+ Иначе
+ Возврат НРег(ИмяТипа);
+ КонецЕсли;
+
+КонецФункции
diff --git "a/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\321\203\320\273\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271\320\241\320\221\320\224.os" "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\321\203\320\273\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271\320\241\320\221\320\224.os"
index f8e9caa2..f7f01a16 100644
--- "a/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\321\203\320\273\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271\320\241\320\221\320\224.os"
+++ "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\321\203\320\273\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271\320\241\320\221\320\224.os"
@@ -15,6 +15,14 @@
//
Перем НомерПоследнейЗаявки;
+// Число - Сколько заявок в очереди еще чего-то ждут.
+//
+// Считать длину очереди нельзя: снятая заявка остается в ней до ближайшей уборки, а уборку делает
+// только тот, кто очередь разбирает. Поток, уже получивший соединение, числился бы ожидающим - и
+// это видели бы наблюдатели в снимке пула.
+//
+Перем ЖивыхЗаявок;
+
// Соответствие - Поток исполнения и выданное ему соединение.
//
Перем СоединенияПотоковИсполнения;
@@ -74,6 +82,11 @@
//
Перем Лог;
+// НаблюдателиИсточникаДанных - Наблюдатели источника данных: получают события операций,
+// запросов, соединений и транзакций, идущих через пул.
+//
+Перем Наблюдатели;
+
// Пул соединений с базой данных.
//
// Через пул идут все обращения к БД. Соединение выдается потоку исполнения на время операции
@@ -150,6 +163,7 @@
);
НомерПоследнейЗаявки = 0;
+ ЖивыхЗаявок = 0;
СоединенияПотоковИсполнения = Новый Соответствие();
ГлубиныЗахвата = Новый Соответствие();
Блокировка = Новый БлокировкаЧтенияЗаписи();
@@ -158,6 +172,51 @@
Закрыт = Ложь;
ПаузаОжидания = ППаузаОжидания;
ПределОжидания = ППределОжидания;
+ Наблюдатели = Новый НаблюдателиИсточникаДанных(ПТипКоннектора);
+
+КонецПроцедуры
+
+// Подменяет реестр наблюдателей пула реестром источника данных.
+//
+// Вызывается сразу после создания пула, пока ни одно соединение не открыто: коннекторы
+// получают реестр при открытии, и подменять его позже нельзя.
+//
+// Параметры:
+// ПНаблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей источника данных.
+//
+Процедура УстановитьНаблюдателей(Знач ПНаблюдатели) Экспорт
+ Наблюдатели = ПНаблюдатели;
+КонецПроцедуры
+
+// Возвращает реестр наблюдателей пула.
+//
+// Возвращаемое значение:
+// НаблюдателиИсточникаДанных - Наблюдатели источника данных, которому принадлежит пул.
+//
+Функция Наблюдатели() Экспорт
+ Возврат Наблюдатели;
+КонецФункции
+
+// Захватывает блокировку записи, отложив рассылку событий.
+//
+// Под блокировкой события случаются - возврат соединения завершает брошенную транзакцию, а ее
+// откат идет запросом к СУБД, - но рассылать их отсюда нельзя: наблюдатель это чужой код, и
+// пока он работает, пул стоял бы целиком. Поэтому события копятся и уходят наблюдателям сразу
+// после снятия блокировки, в РазблокироватьЗапись.
+//
+Процедура ЗаблокироватьЗапись()
+
+ Наблюдатели.НачатьОтложеннуюРассылку();
+ Блокировка.Запись().Заблокировать();
+
+КонецПроцедуры
+
+// Снимает блокировку записи и рассылает накопившиеся события.
+//
+Процедура РазблокироватьЗапись()
+
+ Блокировка.Запись().Разблокировать();
+ Наблюдатели.РазослатьОтложенное();
КонецПроцедуры
@@ -177,24 +236,37 @@
ПотокИсполнения = Потоки.Текущий();
- Блокировка.Запись().Заблокировать();
+ // Возврат соединения за умершим потоком - такое же освобождение, как и явное, и начинается
+ // оно здесь: см. Освободить.
+ НачалоОсвобождения = ТекущаяУниверсальнаяДатаВМиллисекундах();
+
+ ЗаблокироватьЗапись();
+
+ ИтогОсвобождения = Конструктор_ИтогОсвобождения();
// Подписчиков у завершения потока столько, сколько пулов и источников его застали, и движок
// обходит их одним циклом, не перехватывая ошибки обработчиков. Выпусти этот обработчик
// исключение, и до подписавшихся позже очередь не дойдет: их соединения так и остались бы
// за завершившимся потоком. Ошибка уходит в лог: поток кончился, и сообщать о ней некому.
Попытка
- ОсвободитьРесурсыПотокаИсполнения(ПотокИсполнения);
- ОшибкаОсвобождения = "";
+ ОсвободитьРесурсыПотокаИсполнения(ПотокИсполнения, ИтогОсвобождения);
+ ОшибкаОсвобождения = Неопределено;
Исключение
- ОшибкаОсвобождения = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
+ ОшибкаОсвобождения = ИнформацияОбОшибке();
КонецПопытки;
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
+
+ // Наблюдателям интересно возвращение соединения в пул, а чье оно было - живого потока или
+ // умершего - нет: без этого события его захват остался бы без пары.
+ ОпубликоватьОсвобождение(ИтогОсвобождения, НачалоОсвобождения, ОшибкаОсвобождения);
// Запись в лог - вне блокировки: держать ее ради журнала незачем.
- Если НЕ ПустаяСтрока(ОшибкаОсвобождения) Тогда
- Лог.Ошибка("Не удалось освободить ресурсы завершившегося потока исполнения: %1", ОшибкаОсвобождения);
+ Если ОшибкаОсвобождения <> Неопределено Тогда
+ Лог.Ошибка(
+ "Не удалось освободить ресурсы завершившегося потока исполнения: %1",
+ ПодробноеПредставлениеОшибки(ОшибкаОсвобождения)
+ );
КонецЕсли;
КонецПроцедуры
@@ -220,7 +292,14 @@
// сообщит сам поток исполнения. Повторная подписка тем же обработчиком ничего не добавляет.
ДобавитьОбработчик ПотокИсполнения.ПриЗавершении, ПриЗавершенииПотокаИсполнения;
- Блокировка.Запись().Заблокировать();
+ // Захват начинается здесь, а событие появится ниже: пока пул не отработал под блокировкой,
+ // неизвестно даже, нужно ли событие - углубление захвата его не порождает. Рассылать начало
+ // события из-под блокировки нельзя: наблюдатель - чужой код, а на блокировке стоит весь пул.
+ // Поэтому событие создается после, но с этим моментом начала: время, проведенное на
+ // блокировке, входит в захват соединения так же, как ожидание в очереди.
+ НачалоЗахвата = ТекущаяУниверсальнаяДатаВМиллисекундах();
+
+ ЗаблокироватьЗапись();
Попытка
Итог = ПопытатьсяЗанять(ПотокИсполнения);
@@ -228,21 +307,40 @@
Заявка = ВстатьВОчередь(ПотокИсполнения);
КонецЕсли;
Исключение
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
ВызватьИсключение;
КонецПопытки;
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
- Если Итог.Соединение <> Неопределено Тогда
+ // Углубление захвата соединения не меняет и наблюдателям не интересно.
+ Если Итог.Повторно Тогда
Возврат Итог.Соединение;
КонецЕсли;
- Если Итог.Бронь Тогда
- Возврат ОткрытьПоБрони(ПотокИсполнения);
- КонецЕсли;
+ Событие = Наблюдатели.СоздатьСобытие(ВидыСобытийИсточникаДанных.Соединение(), "Занять", 0, НачалоЗахвата);
+ Наблюдатели.НачатьСобытие(Событие);
+
+ ИтогЗахвата = Конструктор_ИтогЗахвата();
+
+ Попытка
- Возврат ДождатьсяПоЗаявке(ПотокИсполнения, Заявка);
+ Если Итог.Соединение <> Неопределено Тогда
+ ИтогЗахвата.Соединение = Итог.Соединение;
+ ИначеЕсли Итог.Бронь Тогда
+ ОткрытьПоБрониСЗамером(ПотокИсполнения, ИтогЗахвата);
+ Иначе
+ ДождатьсяПоЗаявке(ПотокИсполнения, Заявка, ИтогЗахвата);
+ КонецЕсли;
+
+ Исключение
+ ЗавершитьСобытиеСоединения(Событие, ИтогЗахвата, ИнформацияОбОшибке());
+ ВызватьИсключение;
+ КонецПопытки;
+
+ ЗавершитьСобытиеСоединения(Событие, ИтогЗахвата);
+
+ Возврат ИтогЗахвата.Соединение;
КонецФункции
@@ -255,16 +353,30 @@
ПотокИсполнения = Потоки.Текущий();
- Блокировка.Запись().Заблокировать();
+ // Как и захват, освобождение начинается до блокировки: под ней идет и возврат соединения,
+ // и закрытие непригодного - обращение к СУБД. Нужно ли событие, известно только после,
+ // поэтому оно создается ниже, но с этим моментом начала.
+ НачалоОсвобождения = ТекущаяУниверсальнаяДатаВМиллисекундах();
+
+ ЗаблокироватьЗапись();
+
+ ИтогОсвобождения = Конструктор_ИтогОсвобождения();
Попытка
- ОсвободитьЗахват(ПотокИсполнения);
+ ОсвободитьЗахват(ПотокИсполнения, ИтогОсвобождения);
Исключение
- Блокировка.Запись().Разблокировать();
+ ОшибкаОсвобождения = ИнформацияОбОшибке();
+ РазблокироватьЗапись();
+ // Соединение к этому моменту могло быть уже отобрано у потока: упасть возврат может и
+ // на закрытии непригодного соединения, когда пулу оно больше не принадлежит.
+ ОпубликоватьОсвобождение(ИтогОсвобождения, НачалоОсвобождения, ОшибкаОсвобождения);
ВызватьИсключение;
КонецПопытки;
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
+
+ // Наблюдателям интересно возвращение соединения в пул, а не снятие одного уровня захвата.
+ ОпубликоватьОсвобождение(ИтогОсвобождения, НачалоОсвобождения);
КонецПроцедуры
@@ -361,7 +473,7 @@
Функция КоличествоОжидающих() Экспорт
Блокировка.Чтение().Заблокировать();
- Результат = Ожидающие.Количество();
+ Результат = ЖивыхЗаявок;
Блокировка.Чтение().Разблокировать();
Возврат Результат;
@@ -376,7 +488,7 @@
//
Процедура Закрыть() Экспорт
- Блокировка.Запись().Заблокировать();
+ ЗаблокироватьЗапись();
Попытка
@@ -412,7 +524,7 @@
Заявка.Соединение = Неопределено;
СозданоСоединений = СозданоСоединений - 1;
КонецЕсли;
- Заявка.Снята = Истина;
+ СнятьЗаявку(Заявка);
Ожидающие.Взять();
КонецЦикла;
@@ -426,13 +538,13 @@
Исключение
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
ВызватьИсключение;
КонецПопытки;
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
КонецПроцедуры
@@ -475,6 +587,7 @@
Если Соединение <> Неопределено Тогда
ГлубиныЗахвата.Вставить(ПотокИсполнения, ГлубиныЗахвата.Получить(ПотокИсполнения) + 1);
Итог.Соединение = Соединение;
+ Итог.Повторно = Истина;
Возврат Итог;
КонецЕсли;
@@ -537,7 +650,7 @@
Функция ОткрытьПоБрони(Знач ПотокИсполнения)
Попытка
- Соединение = Новый СоединениеСБД(ТипКоннектора, СтрокаСоединения, ПараметрыКоннектора);
+ Соединение = Новый СоединениеСБД(ТипКоннектора, СтрокаСоединения, ПараметрыКоннектора, Наблюдатели);
Исключение
СнятьБронь(ПотокИсполнения);
ВызватьИсключение;
@@ -554,7 +667,7 @@
//
Процедура СнятьБронь(Знач ПотокИсполнения)
- Блокировка.Запись().Заблокировать();
+ ЗаблокироватьЗапись();
Попытка
Если Брони.Получить(ПотокИсполнения) <> Неопределено Тогда
@@ -562,11 +675,11 @@
СозданоСоединений = СозданоСоединений - 1;
КонецЕсли;
Исключение
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
ВызватьИсключение;
КонецПопытки;
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
КонецПроцедуры
@@ -586,7 +699,7 @@
//
Функция ЗакрепитьОткрытое(Знач ПотокИсполнения, Соединение)
- Блокировка.Запись().Заблокировать();
+ ЗаблокироватьЗапись();
Попытка
@@ -605,11 +718,11 @@
КонецЕсли;
Исключение
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
ВызватьИсключение;
КонецПопытки;
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
Если ПулЗакрыт Тогда
Соединение.Закрыть();
@@ -638,59 +751,88 @@
Заявка.Соединение = Неопределено;
Заявка.Снята = Ложь;
Ожидающие.Положить(Заявка);
+ ЖивыхЗаявок = ЖивыхЗаявок + 1;
Возврат Заявка;
КонецФункции
+// Снимает заявку: она больше ничего не ждет.
+//
+// Из очереди заявка при этом не вынимается - вынуть ее оттуда можно только разбором всей очереди,
+// а он и так случится при ближайшей уборке. Снятие идемпотентно: считать живые заявки можно только
+// по переходу.
+//
+// Вызывается при захваченной блокировке записи.
+//
+// Параметры:
+// Заявка - Структура - Заявка, поданная в очередь ожидающих.
+//
+Процедура СнятьЗаявку(Заявка)
+
+ Если Заявка.Снята Тогда
+ Возврат;
+ КонецЕсли;
+
+ Заявка.Снята = Истина;
+ ЖивыхЗаявок = ЖивыхЗаявок - 1;
+
+КонецПроцедуры
+
// Ждет, пока по заявке выдадут соединение.
//
// Параметры:
// ПотокИсполнения - ПотокИсполнения - Поток, для которого выполняется операция.
// Заявка - Структура - Заявка, поданная в очередь ожидающих.
//
-// Возвращаемое значение:
-// СоединениеСБД - Соединение, закрепленное за потоком исполнения.
+// ИтогЗахвата - Структура - Куда записать соединение, ожидание и сведения об открытии,
+// см. Конструктор_ИтогЗахвата.
//
-Функция ДождатьсяПоЗаявке(Знач ПотокИсполнения, Заявка)
+Процедура ДождатьсяПоЗаявке(Знач ПотокИсполнения, Заявка, ИтогЗахвата)
- КрайнийСрок = ТекущаяУниверсальнаяДатаВМиллисекундах() + ПределОжидания;
+ НачалоОжидания = ТекущаяУниверсальнаяДатаВМиллисекундах();
+ КрайнийСрок = НачалоОжидания + ПределОжидания;
Пока Истина Цикл
// Ждать нужно вне блокировки, иначе освободить соединение стало бы некому.
Приостановить(ПаузаОжидания);
- Блокировка.Запись().Заблокировать();
+ ЗаблокироватьЗапись();
Попытка
Итог = ПопытатьсяПолучитьПоЗаявке(Заявка, ПотокИсполнения);
Исключение
// Иначе выданное по брошенной заявке соединение не забрал бы никто.
- Заявка.Снята = Истина;
- Блокировка.Запись().Разблокировать();
+ СнятьЗаявку(Заявка);
+ РазблокироватьЗапись();
ВызватьИсключение;
КонецПопытки;
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
Если Итог.Соединение <> Неопределено Тогда
- Возврат Итог.Соединение;
+ ИтогЗахвата.Ожидание = ТекущаяУниверсальнаяДатаВМиллисекундах() - НачалоОжидания;
+ ИтогЗахвата.Соединение = Итог.Соединение;
+ Возврат;
КонецЕсли;
// Место освободилось, пока ждали: открываем свое соединение так же, как обычный захват -
// без блокировки.
Если Итог.Бронь Тогда
- Возврат ОткрытьПоБрони(ПотокИсполнения);
+ ИтогЗахвата.Ожидание = ТекущаяУниверсальнаяДатаВМиллисекундах() - НачалоОжидания;
+ ОткрытьПоБрониСЗамером(ПотокИсполнения, ИтогЗахвата);
+ Возврат;
КонецЕсли;
Если ТекущаяУниверсальнаяДатаВМиллисекундах() >= КрайнийСрок Тогда
+ ИтогЗахвата.Ожидание = ТекущаяУниверсальнаяДатаВМиллисекундах() - НачалоОжидания;
ОтказатьВОжидании(Заявка);
КонецЕсли;
КонецЦикла;
-КонецФункции
+КонецПроцедуры
// Снимает заявку по истечении предела ожидания и завершает захват ошибкой.
//
@@ -705,11 +847,11 @@
//
Процедура ОтказатьВОжидании(Заявка)
- Блокировка.Запись().Заблокировать();
+ ЗаблокироватьЗапись();
Попытка
- Заявка.Снята = Истина;
+ СнятьЗаявку(Заявка);
// Соединение могли назначить заявке в последний момент - между проверкой и этой строкой.
// В снятой заявке забрать его больше некому.
@@ -720,11 +862,11 @@
КонецЕсли;
Исключение
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
ВызватьИсключение;
КонецПопытки;
- Блокировка.Запись().Разблокировать();
+ РазблокироватьЗапись();
ВызватьИсключение СтрШаблон(
"Поток исполнения не дождался свободного соединения за %1 мс",
@@ -750,7 +892,7 @@
Функция ПопытатьсяПолучитьПоЗаявке(Заявка, Знач ПотокИсполнения)
Если Заявка.Соединение <> Неопределено Тогда
- Заявка.Снята = Истина;
+ СнятьЗаявку(Заявка);
ЗакрепитьЗаПотокомИсполнения(ПотокИсполнения, Заявка.Соединение);
Итог = Конструктор_ИтогПопытки();
@@ -765,7 +907,7 @@
КонецЕсли;
// Заявка больше не нужна: выданное по ней соединение осталось бы невостребованным.
- Заявка.Снята = Истина;
+ СнятьЗаявку(Заявка);
// Пока шла попытка, соединение могли назначить этой же заявке: завершившийся поток вернул
// свое соединение в пул, а оттуда оно ушло в очередь ожидающих. В снятой заявке такое
@@ -799,8 +941,9 @@
//
// Параметры:
// ПотокИсполнения - ПотокИсполнения - Поток, для которого выполняется операция.
+// ИтогОсвобождения - Структура - См. Конструктор_ИтогОсвобождения.
//
-Процедура ОсвободитьЗахват(Знач ПотокИсполнения)
+Процедура ОсвободитьЗахват(Знач ПотокИсполнения, ИтогОсвобождения)
Глубина = ГлубиныЗахвата.Получить(ПотокИсполнения);
Если Глубина = Неопределено Тогда
@@ -822,7 +965,7 @@
Возврат;
КонецЕсли;
- ОтобратьСоединение(ПотокИсполнения);
+ ОтобратьСоединение(ПотокИсполнения, ИтогОсвобождения);
КонецПроцедуры
@@ -831,14 +974,20 @@
//
// Параметры:
// ПотокИсполнения - ПотокИсполнения - Поток, для которого выполняется операция.
+// ИтогОсвобождения - Структура - См. Конструктор_ИтогОсвобождения.
//
-Процедура ОтобратьСоединение(Знач ПотокИсполнения)
+Процедура ОтобратьСоединение(Знач ПотокИсполнения, ИтогОсвобождения)
Соединение = СоединенияПотоковИсполнения.Получить(ПотокИсполнения);
ГлубиныЗахвата.Удалить(ПотокИсполнения);
СоединенияПотоковИсполнения.Удалить(ПотокИсполнения);
+ // Соединение у потока исполнения отобрано, и дальше оно ему не вернется: для наблюдателей
+ // освобождение состоялось. Отметка стоит до возврата в пул, потому что возврат закрывает
+ // непригодное соединение и вместе с закрытием может упасть.
+ ИтогОсвобождения.Возвращено = Истина;
+
ВернутьВПул(Соединение);
КонецПроцедуры
@@ -937,11 +1086,14 @@
//
// Параметры:
// ПотокИсполнения - ПотокИсполнения - Поток, чьи ресурсы освобождаются.
+// ИтогОсвобождения - Структура - См. Конструктор_ИтогОсвобождения. Соединение, назначенное заявке
+// такого потока, освобождением не считается: его захват не состоялся, и событие захвата,
+// которому нужна пара, не создавалось.
//
-Процедура ОсвободитьРесурсыПотокаИсполнения(Знач ПотокИсполнения)
+Процедура ОсвободитьРесурсыПотокаИсполнения(Знач ПотокИсполнения, ИтогОсвобождения)
Если СоединенияПотоковИсполнения.Получить(ПотокИсполнения) <> Неопределено Тогда
- ОтобратьСоединение(ПотокИсполнения);
+ ОтобратьСоединение(ПотокИсполнения, ИтогОсвобождения);
КонецЕсли;
// Поток, оборвавшийся посреди открытия соединения, свою бронь не снимет, а место в пуле
@@ -986,7 +1138,7 @@
КонецЕсли;
Если Заявка.ПотокИсполнения = ПотокИсполнения Тогда
- Заявка.Снята = Истина;
+ СнятьЗаявку(Заявка);
Брошенные.Добавить(Заявка);
Иначе
Ожидающие.Положить(Заявка);
@@ -1018,7 +1170,132 @@
Итог = Новый Структура;
Итог.Вставить("Соединение");
Итог.Вставить("Бронь", Ложь);
+ // Истина, если поток исполнения уже держал соединение и захват лишь углубился.
+ Итог.Вставить("Повторно", Ложь);
Возврат Итог;
КонецФункции
+
+// Сведения о том, как поток исполнения получил соединение: для события наблюдателей.
+//
+// Возвращаемое значение:
+// Структура - Соединение, Открыто (открыто ли новое), ВремяОткрытия и Ожидание в миллисекундах.
+//
+Функция Конструктор_ИтогЗахвата()
+
+ Итог = Новый Структура;
+ Итог.Вставить("Соединение");
+ Итог.Вставить("Открыто", Ложь);
+ Итог.Вставить("ВремяОткрытия", 0);
+ Итог.Вставить("Ожидание", 0);
+
+ Возврат Итог;
+
+КонецФункции
+
+// Сведения о том, отдал ли поток исполнения соединение пулу: для события наблюдателей.
+//
+// Итог отдельной структурой, а не возвращаемым значением, потому что возврат может завершиться
+// ошибкой уже после того, как соединение отобрано у потока: закрытие непригодного соединения идет
+// в СУБД и падает вместе с ней. Событие освобождения нужно и тогда - иначе событие захвата
+// останется без пары, - а из результата, которого не будет, об этом не узнать.
+//
+// Возвращаемое значение:
+// Структура - Возвращено: Истина, если соединение отобрано у потока исполнения.
+//
+Функция Конструктор_ИтогОсвобождения()
+
+ Итог = Новый Структура;
+ Итог.Вставить("Возвращено", Ложь);
+
+ Возврат Итог;
+
+КонецФункции
+
+// Открывает соединение по брони, замеряя время открытия.
+//
+// Параметры:
+// ПотокИсполнения - ПотокИсполнения - Поток, для которого выполняется операция.
+// ИтогЗахвата - Структура - Куда записать соединение и время открытия.
+//
+Процедура ОткрытьПоБрониСЗамером(Знач ПотокИсполнения, ИтогЗахвата)
+
+ НачалоОткрытия = ТекущаяУниверсальнаяДатаВМиллисекундах();
+ ИтогЗахвата.Соединение = ОткрытьПоБрони(ПотокИсполнения);
+ ИтогЗахвата.Открыто = Истина;
+ ИтогЗахвата.ВремяОткрытия = ТекущаяУниверсальнаяДатаВМиллисекундах() - НачалоОткрытия;
+
+КонецПроцедуры
+
+// Рассылает наблюдателям событие освобождения соединения, если оно состоялось.
+//
+// Зовется после снятия блокировки пула: снимок пула берется чтением, а наблюдатель - чужой код,
+// которому под блокировкой работать нельзя.
+//
+// Параметры:
+// ИтогОсвобождения - Структура - См. Конструктор_ИтогОсвобождения.
+// НачалоОсвобождения - Число - Универсальная дата входа в пул в миллисекундах.
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка возврата, обычно неудачного
+// закрытия непригодного соединения.
+//
+Процедура ОпубликоватьОсвобождение(
+ ИтогОсвобождения,
+ Знач НачалоОсвобождения,
+ Знач ИнформацияОбОшибке = Неопределено
+)
+
+ Если НЕ ИтогОсвобождения.Возвращено Тогда
+ Возврат;
+ КонецЕсли;
+
+ Событие = Наблюдатели.СоздатьСобытие(
+ ВидыСобытийИсточникаДанных.Соединение(), "Освободить", 0, НачалоОсвобождения);
+ Наблюдатели.НачатьСобытие(Событие);
+ ЗавершитьСобытиеСоединения(Событие, Неопределено, ИнформацияОбОшибке);
+
+КонецПроцедуры
+
+// Завершает событие захвата соединения снимком пула.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных, Неопределено - Событие захвата.
+// ИтогЗахвата - Структура - См. Конструктор_ИтогЗахвата.
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка захвата.
+//
+Процедура ЗавершитьСобытиеСоединения(Событие, ИтогЗахвата, Знач ИнформацияОбОшибке = Неопределено)
+
+ Если Событие = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ Если ИтогЗахвата <> Неопределено Тогда
+ Событие.УстановитьДанныеЗахвата(ИтогЗахвата.Ожидание, ИтогЗахвата.Открыто, ИтогЗахвата.ВремяОткрытия);
+ КонецЕсли;
+
+ Снимок = СнимокПула();
+ Событие.УстановитьСнимокПула(Снимок.Занято, Снимок.Свободно, Снимок.Ожидает, Снимок.Предел);
+ Наблюдатели.ЗавершитьСобытие(Событие, ИнформацияОбОшибке);
+
+КонецПроцедуры
+
+// Снимает состояние пула одним чтением, чтобы значения снимка были согласованы между собой.
+//
+// Возвращаемое значение:
+// Структура - Занято, Свободно, Ожидает, Предел: см. КоличествоЗанятых, КоличествоСвободных,
+// КоличествоОжидающих и размер пула.
+//
+Функция СнимокПула()
+
+ Снимок = Новый Структура;
+
+ Блокировка.Чтение().Заблокировать();
+ Снимок.Вставить("Занято", СоединенияПотоковИсполнения.Количество());
+ Снимок.Вставить("Свободно", Свободные.Количество());
+ Снимок.Вставить("Ожидает", ЖивыхЗаявок);
+ Снимок.Вставить("Предел", РазмерПула);
+ Блокировка.Чтение().Разблокировать();
+
+ Возврат Снимок;
+
+КонецФункции
diff --git "a/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\265\320\241\320\221\320\224.os" "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\265\320\241\320\221\320\224.os"
index 0b93c0d1..4cbd711c 100644
--- "a/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\265\320\241\320\221\320\224.os"
+++ "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\265\320\241\320\221\320\224.os"
@@ -7,6 +7,11 @@
// Число - Момент последнего возврата в пул в миллисекундах. По нему пул выбирает, какое
// соединение выдать следующим.
Перем МоментВозврата;
+// НаблюдателиИсточникаДанных - Наблюдатели пула: получают события транзакций этого соединения.
+Перем НаблюдателиПула;
+// Массив из СобытиеИсточникаДанных - Стопка событий транзакций: каждое начало кладет свое
+// событие, каждое завершение снимает верхнее. Пуста вне транзакции и без наблюдателей.
+Перем СобытияТранзакций;
// Соединение с базой данных, выданное из пула соединений.
//
@@ -21,12 +26,25 @@
// ТипКоннектора - Тип - Тип класса, реализующего интерфейс коннектора.
// СтрокаСоединения - Строка - Строка соединения с БД.
// ПараметрыКоннектора - Массив - Дополнительные параметры коннектора.
+// Наблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей пула: коннектор, объявивший
+// НаблюдаемыйКоннектор, получает его после открытия.
//
-Процедура ПриСозданииОбъекта(Знач ТипКоннектора, Знач СтрокаСоединения, Знач ПараметрыКоннектора)
+Процедура ПриСозданииОбъекта(Знач ТипКоннектора, Знач СтрокаСоединения, Знач ПараметрыКоннектора, Знач Наблюдатели)
Коннектор = РаботаСКоннекторами.СоздатьКоннектор(ТипКоннектора);
РаботаСКоннекторами.ОткрытьКоннектор(Коннектор, СтрокаСоединения, ПараметрыКоннектора);
+ // Коннектор уже открыт: ошибка подключения наблюдателей не должна оставить его без закрытия.
+ Попытка
+ Наблюдатели.ПодключитьКоннектор(Коннектор);
+ Исключение
+ РаботаСКоннекторами.ЗакрытьКоннектор(Коннектор);
+ ВызватьИсключение;
+ КонецПопытки;
+
+ НаблюдателиПула = Наблюдатели;
+
+ СобытияТранзакций = Новый Массив;
ВТранзакции = Ложь;
Испорчено = Ложь;
ОтметитьВозврат();
@@ -49,7 +67,19 @@
//
Процедура НачатьТранзакцию() Экспорт
- РаботаСКоннекторами.НачатьТранзакцию(Коннектор);
+ // Событие начинается до BEGIN и живет до исхода: фиксации, отмены, неудачи - в том числе
+ // самого начала - или отката брошенной транзакции. Свое событие получает каждое начало,
+ // в том числе вложенное: в трассировке вложенный вызов должен быть виден отдельным спаном.
+ НачатьСобытиеТранзакции();
+
+ Попытка
+ РаботаСКоннекторами.НачатьТранзакцию(Коннектор);
+ Исключение
+ // Неудачное начало закрывает только свое событие: начатая раньше транзакция жива.
+ ЗавершитьВерхнееСобытиеТранзакции("failed", ИнформацияОбОшибке());
+ ВызватьИсключение;
+ КонецПопытки;
+
ВТранзакции = Истина;
КонецПроцедуры
@@ -63,10 +93,12 @@
РаботаСКоннекторами.ЗафиксироватьТранзакцию(Коннектор);
Исключение
ИспортитьПослеНеудачногоЗавершения();
+ ЗавершитьВерхнееСобытиеТранзакции("failed", ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
ВТранзакции = Ложь;
+ ЗавершитьВерхнееСобытиеТранзакции("commit");
КонецПроцедуры
@@ -79,10 +111,52 @@
РаботаСКоннекторами.ОтменитьТранзакцию(Коннектор);
Исключение
ИспортитьПослеНеудачногоЗавершения();
+ ЗавершитьВерхнееСобытиеТранзакции("failed", ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
ВТранзакции = Ложь;
+ ЗавершитьВерхнееСобытиеТранзакции("rollback");
+
+КонецПроцедуры
+
+// Начинает событие очередной транзакции и кладет его на вершину стопки.
+//
+// Без наблюдателей события нет: стопка остается пустой, завершать нечего.
+//
+Процедура НачатьСобытиеТранзакции()
+
+ Событие = НаблюдателиПула.СоздатьСобытие(ВидыСобытийИсточникаДанных.Транзакция(), "Транзакция");
+ Если Событие = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ СобытияТранзакций.Добавить(Событие);
+ НаблюдателиПула.НачатьСобытие(Событие);
+
+КонецПроцедуры
+
+// Снимает со стопки верхнее событие транзакции, завершает его исходом и отдает наблюдателям.
+//
+// Завершение всегда относится к последнему начатому событию: события транзакций вложены строго,
+// как спаны в трассировке.
+//
+// Параметры:
+// Исход - Строка - commit, rollback, failed или abandoned.
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка завершения.
+//
+Процедура ЗавершитьВерхнееСобытиеТранзакции(Знач Исход, Знач ИнформацияОбОшибке = Неопределено)
+
+ Если СобытияТранзакций.Количество() = 0 Тогда
+ Возврат;
+ КонецЕсли;
+
+ Вершина = СобытияТранзакций.ВГраница();
+ Событие = СобытияТранзакций[Вершина];
+ СобытияТранзакций.Удалить(Вершина);
+
+ Событие.УстановитьИсход(Исход);
+ НаблюдателиПула.ЗавершитьСобытие(Событие, ИнформацияОбОшибке);
КонецПроцедуры
@@ -137,13 +211,25 @@
Процедура ОтметитьВозврат() Экспорт
Если ВТранзакции Тогда
+ // Транзакцию бросили: исход для наблюдателей - abandoned, каким бы ни был откат, поэтому
+ // откат идет мимо ОтменитьТранзакцию - та завершила бы событие исходом rollback.
Попытка
- ЭтотОбъект.ОтменитьТранзакцию();
+ РаботаСКоннекторами.ОтменитьТранзакцию(Коннектор);
+ ОшибкаОтката = Неопределено;
Исключение
ИспортитьПослеНеудачногоЗавершения();
+ ОшибкаОтката = ИнформацияОбОшибке();
КонецПопытки;
+ ВТранзакции = Ложь;
+ ЗавершитьВерхнееСобытиеТранзакции("abandoned", ОшибкаОтката);
КонецЕсли;
+ // Остаток стопки - события начал, которым не досталось своего завершения: их закрывает
+ // возврат соединения, потому что дальше оно уйдет другому потоку исполнения.
+ Пока СобытияТранзакций.Количество() > 0 Цикл
+ ЗавершитьВерхнееСобытиеТранзакции("abandoned");
+ КонецЦикла;
+
МоментВозврата = ТекущаяУниверсальнаяДатаВМиллисекундах();
КонецПроцедуры
diff --git "a/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\320\277\320\265\321\200\320\260\321\206\320\270\320\270\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os" "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\320\277\320\265\321\200\320\260\321\206\320\270\320\270\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
new file mode 100644
index 00000000..a81f36d6
--- /dev/null
+++ "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\236\320\277\320\265\321\200\320\260\321\206\320\270\320\270\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
@@ -0,0 +1,81 @@
+// Соединение под операцию источника данных: захват и возврат, парные событию операции.
+//
+// Обе половины протокола операции живут здесь: соединение берется под уже начатое событие и
+// возвращается вместе с его завершением. Коннектора модуль не касается - он о пуле и наблюдателях.
+
+#Использовать logos
+
+// Лог - Журнал операций источника данных.
+Перем Лог;
+
+// Занимает соединение для начатой операции.
+//
+// Захват может не удаться - пул закрыт или предел ожидания исчерпан. Такая операция должна
+// завершиться для наблюдателей ошибкой, а не остаться начатой навсегда.
+//
+// Параметры:
+// Пул - ПулСоединенийСБД - Пул, из которого берется соединение.
+// Событие - СобытиеИсточникаДанных, Неопределено - Событие операции из НачатьОперацию.
+//
+// Возвращаемое значение:
+// СоединениеСБД - Соединение текущего потока исполнения.
+//
+Функция ЗанятьДляОперации(Пул, Событие) Экспорт
+
+ Попытка
+ Соединение = Пул.Занять();
+ Исключение
+ Пул.Наблюдатели().ЗавершитьОперацию(Событие, ИнформацияОбОшибке());
+ ВызватьИсключение;
+ КонецПопытки;
+
+ Возврат Соединение;
+
+КонецФункции
+
+// Возвращает соединение операции в пул и завершает событие операции.
+//
+// Освобождение может не удаться: пул закрывает непригодное соединение, а закрытие идет в СУБД
+// и падает вместе с ней. Событие операции завершается в любом случае - иначе наблюдатель не
+// получит ПослеСобытия, а вложенность операций потока так и останется увеличенной.
+//
+// Ошибка освобождения идет наружу, только если операция прошла успешно. Упади они обе, наружу
+// уходит ошибка операции: она первопричина, а неудачное освобождение - ее следствие.
+//
+// Параметры:
+// Пул - ПулСоединенийСБД - Пул, которому возвращается соединение.
+// Наблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей пула.
+// Событие - СобытиеИсточникаДанных, Неопределено - Событие операции из НачатьОперацию.
+// ОшибкаОперации - ИнформацияОбОшибке, Неопределено - Ошибка, которой завершилась операция.
+// КоличествоСтрок - Число, Неопределено - Число прочитанных операцией строк.
+//
+Процедура ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие,
+ Знач ОшибкаОперации = Неопределено, Знач КоличествоСтрок = Неопределено) Экспорт
+
+ Попытка
+ Пул.Освободить();
+ Исключение
+
+ Если ОшибкаОперации <> Неопределено Тогда
+ Лог.Ошибка(
+ "Не удалось освободить соединение после ошибки операции: %1",
+ ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())
+ );
+ Наблюдатели.ЗавершитьОперацию(Событие, ОшибкаОперации);
+ Возврат;
+ КонецЕсли;
+
+ Наблюдатели.ЗавершитьОперацию(Событие, ИнформацияОбОшибке(), КоличествоСтрок);
+ ВызватьИсключение;
+
+ КонецПопытки;
+
+ Наблюдатели.ЗавершитьОперацию(Событие, ОшибкаОперации, КоличествоСтрок);
+
+КонецПроцедуры
+
+Процедура Инициализация()
+ Лог = Логирование.ПолучитьЛог("oscript.lib.entity.operations");
+КонецПроцедуры
+
+Инициализация();
diff --git "a/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\274\320\270.os" "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\274\320\270.os"
index 17b0ec33..208d5b5e 100644
--- "a/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\274\320\270.os"
+++ "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\274\320\270.os"
@@ -31,20 +31,27 @@
ПроверитьНеобходимостьЗаполненияИдентификатора(ОбъектМодели, Сущность);
Пул = Менеджер.ИсточникДанных().ПулСоединений();
- Соединение = Пул.Занять();
+ Наблюдатели = Пул.Наблюдатели();
+ Событие = Наблюдатели.НачатьОперацию("Сохранить", ОбъектМодели);
+ Соединение = ОперацииИсточникаДанных.ЗанятьДляОперации(Пул, Событие);
Попытка
+
Коннектор = Соединение.ПолучитьКоннектор();
Коннектор.Сохранить(ОбъектМодели, Сущность);
СохранитьПодчиненныеТаблицы(Коннектор, ОбъектМодели, Сущность);
+
+ // Пул сущностей обновляется до возврата соединения: экземпляр на идентификатор должен
+ // остаться один, а возврат идет уже после работы с СУБД и может упасть на закрытии
+ // непригодного соединения.
+ ПулСущностей.Вставить(ОбъектМодели.ПолучитьЗначениеИдентификатора(Сущность), Сущность);
+
Исключение
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
- Пул.Освободить();
-
- ПулСущностей.Вставить(ОбъектМодели.ПолучитьЗначениеИдентификатора(Сущность), Сущность);
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие);
КонецПроцедуры
@@ -62,26 +69,50 @@
Функция Получить(Менеджер, ОбъектМодели, ПулСущностей, ОпцииПоиска) Экспорт
ПередаваемыеОпцииПоиска = ПреобразоватьОпцииПоиска(ОбъектМодели.Колонки(), ОпцииПоиска);
+
+ Возврат НайтиСущности(Менеджер, ОбъектМодели, ПулСущностей, ПередаваемыеОпцииПоиска, "Получить");
+
+КонецФункции
+
+// Ищет сущности соединением источника данных и отдает наблюдателям событие своей операции.
+//
+// Операция названа параметром: вызов ПолучитьОдно - такая же одна операция для наблюдателя,
+// а не поиск, вложенный в поиск.
+//
+// Параметры:
+// Менеджер - МенеджерСущностей - Менеджер операции: дает источник и хранилища своих типов.
+// ОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД.
+// ПулСущностей - СинхронизированнаяКарта - Ранее прочитанные экземпляры сущностей.
+// ОпцииПоиска - ОпцииПоиска - Опции поиска в терминах колонок таблицы.
+// ИмяОперации - Строка - Имя операции для наблюдателей.
+//
+// Возвращаемое значение:
+// Массив - Найденные сущности.
+//
+Функция НайтиСущности(Менеджер, ОбъектМодели, ПулСущностей, ОпцииПоиска, Знач ИмяОперации)
+
НайденныеСущности = Новый Массив;
Пул = Менеджер.ИсточникДанных().ПулСоединений();
- Соединение = Пул.Занять();
+ Наблюдатели = Пул.Наблюдатели();
+ Событие = Наблюдатели.НачатьОперацию(ИмяОперации, ОбъектМодели);
+ Соединение = ОперацииИсточникаДанных.ЗанятьДляОперации(Пул, Событие);
Попытка
Коннектор = Соединение.ПолучитьКоннектор();
- НайденныеСтроки = Коннектор.НайтиСтрокиВТаблице(ОбъектМодели, ПередаваемыеОпцииПоиска);
+ НайденныеСтроки = Коннектор.НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
Для Каждого НайденнаяСтрока Из НайденныеСтроки Цикл
НайденныеСущности.Добавить(СобратьСущность(Коннектор, Менеджер, ОбъектМодели, ПулСущностей, НайденнаяСтрока));
КонецЦикла;
Исключение
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие, , НайденныеСущности.Количество());
Возврат НайденныеСущности;
@@ -108,7 +139,9 @@
ПередаваемыеОпцииПоиска.Отбор(ОбъектМодели.Идентификатор().ИмяПоля, ВидСравнения.Равно, ОпцииПоиска);
КонецЕсли;
- НайденныеСущности = Получить(Менеджер, ОбъектМодели, ПулСущностей, ПередаваемыеОпцииПоиска);
+ ПередаваемыеОпцииПоиска = ПреобразоватьОпцииПоиска(ОбъектМодели.Колонки(), ПередаваемыеОпцииПоиска);
+
+ НайденныеСущности = НайтиСущности(Менеджер, ОбъектМодели, ПулСущностей, ПередаваемыеОпцииПоиска, "ПолучитьОдно");
Если НайденныеСущности.Количество() = 0 Тогда
Возврат Неопределено;
@@ -130,19 +163,24 @@
Процедура Удалить(Менеджер, ОбъектМодели, ПулСущностей, Сущность) Экспорт
Пул = Менеджер.ИсточникДанных().ПулСоединений();
- Соединение = Пул.Занять();
+ Наблюдатели = Пул.Наблюдатели();
+ Событие = Наблюдатели.НачатьОперацию("Удалить", ОбъектМодели);
+ Соединение = ОперацииИсточникаДанных.ЗанятьДляОперации(Пул, Событие);
Попытка
+
Соединение.ПолучитьКоннектор().Удалить(ОбъектМодели, Сущность);
+
+ // Пул сущностей разложен по идентификаторам: удаление по самой сущности не нашло бы
+ // ничего. Как и при сохранении, пул обновляется до возврата соединения.
+ ПулСущностей.Удалить(ОбъектМодели.ПолучитьЗначениеИдентификатора(Сущность));
+
Исключение
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
- Пул.Освободить();
-
- // Пул сущностей разложен по идентификаторам: удаление по самой сущности не нашло бы ничего.
- ПулСущностей.Удалить(ОбъектМодели.ПолучитьЗначениеИдентификатора(Сущность));
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие);
КонецПроцедуры
@@ -162,16 +200,18 @@
Действие = ПостроитьДействие(Алгоритм, Контекст, Объект, ФункциональныеИнтерфейсы.УниПроцедура());
Пул = Источник.ПулСоединений();
- Соединение = Пул.Занять();
+ Наблюдатели = Пул.Наблюдатели();
+ Событие = Наблюдатели.НачатьОперацию("ВыполнитьСКоннектором");
+ Соединение = ОперацииИсточникаДанных.ЗанятьДляОперации(Пул, Событие);
Попытка
Действие.Выполнить(Соединение.ПолучитьКоннектор());
Исключение
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие);
КонецПроцедуры
@@ -195,16 +235,18 @@
Действие = ПостроитьДействие(Алгоритм, Контекст, Объект, ФункциональныеИнтерфейсы.УниФункция());
Пул = Источник.ПулСоединений();
- Соединение = Пул.Занять();
+ Наблюдатели = Пул.Наблюдатели();
+ Событие = Наблюдатели.НачатьОперацию("ВычислитьСКоннектором");
+ Соединение = ОперацииИсточникаДанных.ЗанятьДляОперации(Пул, Событие);
Попытка
Результат = Действие.Выполнить(Соединение.ПолучитьКоннектор());
Исключение
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие);
Возврат Результат;
diff --git "a/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\321\202\321\200\320\276\320\272\320\270\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\321\217.os" "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\321\202\321\200\320\276\320\272\320\270\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\321\217.os"
new file mode 100644
index 00000000..b31d42ba
--- /dev/null
+++ "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\321\202\321\200\320\276\320\272\320\270\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\321\217.os"
@@ -0,0 +1,164 @@
+// Разбор строк соединения вида "Ключ=Значение;Ключ=Значение".
+//
+// Нужен коннекторам, чтобы описать соединение наблюдателям: адрес, порт, база. Ключи
+// сравниваются без учета регистра и пробелов вокруг; значения возвращаются как есть.
+
+// Разбирает строку соединения на пары.
+//
+// Параметры:
+// СтрокаСоединения - Строка - Строка вида "Host=localhost;Port=5432;Database=test".
+//
+// Возвращаемое значение:
+// Соответствие - Ключ в нижнем регистре без пробелов - значение без пробелов по краям.
+//
+Функция Параметры(Знач СтрокаСоединения) Экспорт
+
+ Результат = Новый Соответствие;
+
+ Для Каждого Пара Из СтрРазделить(СтрокаСоединения, ";", Ложь) Цикл
+
+ ПозицияРавно = СтрНайти(Пара, "=");
+ Если ПозицияРавно = 0 Тогда
+ Продолжить;
+ КонецЕсли;
+
+ Ключ = НРег(СокрЛП(Лев(Пара, ПозицияРавно - 1)));
+ Значение = СокрЛП(Сред(Пара, ПозицияРавно + 1));
+
+ Если ЗначениеЗаполнено(Ключ) Тогда
+ Результат.Вставить(Ключ, Значение);
+ КонецЕсли;
+
+ КонецЦикла;
+
+ Возврат Результат;
+
+КонецФункции
+
+// Возвращает первое найденное значение из перечисленных ключей.
+//
+// Параметры:
+// ПараметрыСтроки - Соответствие - Результат Параметры().
+// Ключи - Строка - Ключи через запятую в порядке предпочтения, например "host,server".
+// ЗначениеПоУмолчанию - Произвольный - Что вернуть, если ни одного ключа нет.
+//
+// Возвращаемое значение:
+// Произвольный - Значение первого найденного ключа или значение по умолчанию.
+//
+Функция Значение(Знач ПараметрыСтроки, Знач Ключи, Знач ЗначениеПоУмолчанию = "") Экспорт
+
+ Для Каждого Ключ Из СтрРазделить(Ключи, ",") Цикл
+ Найденное = ПараметрыСтроки.Получить(НРег(СокрЛП(Ключ)));
+ Если Найденное <> Неопределено Тогда
+ Возврат Найденное;
+ КонецЕсли;
+ КонецЦикла;
+
+ Возврат ЗначениеПоУмолчанию;
+
+КонецФункции
+
+// Описание соединения без секретов для коннектора клиент-серверной СУБД.
+//
+// Параметры:
+// Система - Строка - Имя СУБД.
+// СтрокаСоединения - Строка - Строка соединения.
+// ПортПоУмолчанию - Число - Порт, если в строке его нет.
+//
+// Возвращаемое значение:
+// Структура - Система, Адрес, Порт, БазаДанных.
+//
+Функция ОписаниеСервера(Знач Система, Знач СтрокаСоединения, Знач ПортПоУмолчанию) Экспорт
+
+ ПараметрыСтроки = Параметры(СтрокаСоединения);
+
+ Порт = Значение(ПараметрыСтроки, "port", "");
+ Порт = ?(ЭтоЦелоеЧисло(Порт), Число(Порт), ПортПоУмолчанию);
+
+ Возврат Описание(
+ Система,
+ Значение(ПараметрыСтроки, "host,server,data source"),
+ Порт,
+ Значение(ПараметрыСтроки, "database,initial catalog")
+ );
+
+КонецФункции
+
+// Описание соединения для файловой или встроенной СУБД без сервера.
+//
+// Параметры:
+// Система - Строка - Имя СУБД.
+// БазаДанных - Строка - Путь к файлу, каталогу или иное имя базы.
+//
+// Возвращаемое значение:
+// Структура - Система, Адрес, Порт, БазаДанных.
+//
+Функция ОписаниеБезСервера(Знач Система, Знач БазаДанных) Экспорт
+ Возврат Описание(Система, "", 0, БазаДанных);
+КонецФункции
+
+// Отрезает от URI параметры запроса: в них бывают учетные данные, которым в описании не место.
+//
+// Параметры:
+// URI - Строка - URI или путь, например "file:base.db?cache=shared".
+//
+// Возвращаемое значение:
+// Строка - Часть до первого "?", например "file:base.db"; строка без "?" возвращается как есть.
+//
+Функция БезПараметровЗапроса(Знач URI) Экспорт
+
+ ПозицияВопроса = СтрНайти(URI, "?");
+ Если ПозицияВопроса = 0 Тогда
+ Возврат URI;
+ КонецЕсли;
+
+ Возврат Лев(URI, ПозицияВопроса - 1);
+
+КонецФункции
+
+// Собирает описание соединения из его полей.
+//
+// Параметры:
+// Система - Строка - Имя СУБД.
+// Адрес - Строка - Адрес сервера.
+// Порт - Число - Порт сервера.
+// БазаДанных - Строка - Имя базы, путь к файлу или каталогу.
+//
+// Возвращаемое значение:
+// Структура - Система, Адрес, Порт, БазаДанных.
+//
+Функция Описание(Знач Система, Знач Адрес, Знач Порт, Знач БазаДанных)
+
+ Результат = Новый Структура;
+ Результат.Вставить("Система", Система);
+ Результат.Вставить("Адрес", Адрес);
+ Результат.Вставить("Порт", Порт);
+ Результат.Вставить("БазаДанных", БазаДанных);
+
+ Возврат Результат;
+
+КонецФункции
+
+// Проверяет, что текст состоит только из цифр: такой порт приводится к числу без исключения.
+//
+// Параметры:
+// Текст - Строка - Значение параметра строки соединения.
+//
+// Возвращаемое значение:
+// Булево - Истина, если текст непустой и состоит из одних цифр.
+//
+Функция ЭтоЦелоеЧисло(Знач Текст)
+
+ Если ПустаяСтрока(Текст) Тогда
+ Возврат Ложь;
+ КонецЕсли;
+
+ Для Номер = 1 По СтрДлина(Текст) Цикл
+ Если СтрНайти("0123456789", Сред(Текст, Номер, 1)) = 0 Тогда
+ Возврат Ложь;
+ КонецЕсли;
+ КонецЦикла;
+
+ Возврат Истина;
+
+КонецФункции
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.os"
index 8721767b..9fdf764a 100644
--- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.os"
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.os"
@@ -4,6 +4,10 @@
// Конструктор объекта АбстрактныйКоннектор.
//
+// Класс описывает интерфейс коннектора: реализации объявляют его аннотацией
+// &Реализует("АбстрактныйКоннектор") и могут дополнительно объявить НаблюдаемыйКоннектор.
+//
+&Интерфейс
Процедура ПриСозданииОбъекта()
Открыт = Ложь;
КонецПроцедуры
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQL.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQL.os"
index 86b84b22..388e077e 100644
--- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQL.os"
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQL.os"
@@ -3,18 +3,113 @@
Перем Лог;
Перем Коннектор;
+// НаблюдателиИсточникаДанных - Наблюдатели пула, которым сообщается о каждом запросе. Неопределено,
+// пока коннектор работает вне пула.
+Перем Наблюдатели;
Процедура ПриСозданииОбъекта(пКоннектор, пЛог = Неопределено)
-
+
Коннектор = пКоннектор;
Если ЗначениеЗаполнено(пЛог) Тогда
Лог = пЛог;
- Иначе
+ Иначе
Лог = Логирование.ПолучитьЛог("oscript.lib.entity.connector.abstractsql");
КонецЕсли;
КонецПроцедуры
+// Передает реестр наблюдателей, которым сообщается о запросах.
+//
+// Параметры:
+// ПНаблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей пула.
+//
+Процедура УстановитьНаблюдателей(ПНаблюдатели) Экспорт
+ Наблюдатели = ПНаблюдатели;
+КонецПроцедуры
+
+// Выполняет подготовленный запрос, сообщая о нем наблюдателям.
+//
+// Текст и параметры запроса должны быть установлены до вызова. Событие запроса несет текст с
+// плейсхолдерами параметров, число строк результата и ошибку, если запрос ею завершился.
+//
+// Параметры:
+// Запрос - Запрос - Подготовленный запрос к СУБД.
+// ОперацияСУБД - Строка - SELECT, INSERT, DELETE, CREATE TABLE, BEGIN, COMMIT, ROLLBACK.
+// ИмяТаблицы - Строка - Таблица запроса; пустая строка, если таблицы нет.
+// ВернутьРезультат - Булево - Истина - выполнить как выборку и вернуть таблицу значений,
+// Ложь - выполнить как команду.
+//
+// Возвращаемое значение:
+// ТаблицаЗначений - Результат выборки.
+// Неопределено - Запрос выполнен как команда.
+//
+Функция ВыполнитьСНаблюдением(Запрос, Знач ОперацияСУБД, Знач ИмяТаблицы, Знач ВернутьРезультат) Экспорт
+
+ Событие = НачатьЗапрос(ОперацияСУБД, ИмяТаблицы, Запрос.Текст);
+
+ Попытка
+ Если ВернутьРезультат Тогда
+ Результат = Запрос.Выполнить().Выгрузить();
+ Иначе
+ Запрос.ВыполнитьКоманду();
+ Результат = Неопределено;
+ КонецЕсли;
+ Исключение
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
+ ВызватьИсключение;
+ КонецПопытки;
+
+ ЗавершитьЗапрос(Событие, , ?(ВернутьРезультат, Результат.Количество(), Неопределено));
+
+ Возврат Результат;
+
+КонецФункции
+
+Функция НачатьЗапрос(Знач ОперацияСУБД, Знач ИмяТаблицы, Знач ТекстЗапроса)
+
+ Если Наблюдатели = Неопределено Тогда
+ Возврат Неопределено;
+ КонецЕсли;
+
+ Возврат Наблюдатели.НачатьЗапрос(ОперацияСУБД, ИмяТаблицы, ТекстЗапроса);
+
+КонецФункции
+
+Процедура ЗавершитьЗапрос(Событие, Знач ИнформацияОбОшибке = Неопределено, Знач КоличествоСтрок = Неопределено)
+
+ Если Событие = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ Наблюдатели.ЗавершитьЗапрос(Событие, ИнформацияОбОшибке, КоличествоСтрок);
+
+КонецПроцедуры
+
+// Определяет операцию СУБД по первым словам текста запроса: SELECT, DELETE, CREATE TABLE и т.п.
+//
+// Параметры:
+// ТекстЗапроса - Строка - Текст запроса или команды.
+//
+// Возвращаемое значение:
+// Строка - Операция в верхнем регистре; для CREATE, DROP и ALTER вместе со вторым словом.
+//
+Функция ОперацияИзТекста(Знач ТекстЗапроса)
+
+ Слова = СтрРазделить(СокрЛП(СтрЗаменить(ТекстЗапроса, Символы.ПС, " ")), " ", Ложь);
+ Если Слова.Количество() = 0 Тогда
+ Возврат "";
+ КонецЕсли;
+
+ Операция = ВРег(Слова[0]);
+ ОперацияНадСхемой = Операция = "CREATE" ИЛИ Операция = "DROP" ИЛИ Операция = "ALTER";
+ Если ОперацияНадСхемой И Слова.Количество() > 1 Тогда
+ Операция = Операция + " " + ВРег(Слова[1]);
+ КонецЕсли;
+
+ Возврат Операция;
+
+КонецФункции
+
// @Unstable
// Выполнить произвольный запрос и получить результат.
//
@@ -33,8 +128,8 @@
Запрос = Коннектор.КонструкторКоннектора.НовыйЗапрос(Коннектор.Соединение);
Запрос.Текст = ТекстЗапроса;
- Результат = Запрос.Выполнить().Выгрузить();
-
+ Результат = ВыполнитьСНаблюдением(Запрос, ОперацияИзТекста(ТекстЗапроса), "", Истина);
+
Возврат Результат;
КонецФункции
@@ -64,9 +159,9 @@
КонецЕсли;
Лог.Отладка("Удаление сущностей в таблице %1:%2%3", ОбъектМодели.ИмяТаблицы(), Символы.ПС, ТекстЗапроса);
-
+
Запрос.Текст = ТекстЗапроса;
- Запрос.ВыполнитьКоманду();
+ ВыполнитьСНаблюдением(Запрос, "DELETE", ОбъектМодели.ИмяТаблицы(), Ложь);
КонецПроцедуры
@@ -121,8 +216,8 @@
Лог.Отладка("Поиск сущности в таблице %1:%2%3", ОбъектМодели.ИмяТаблицы(), Символы.ПС, ТекстЗапроса);
Запрос.Текст = ТекстЗапроса;
- Результат = Запрос.Выполнить().Выгрузить();
-
+ Результат = ВыполнитьСНаблюдением(Запрос, "SELECT", ОбъектМодели.ИмяТаблицы(), Истина);
+
Если Результат.Количество() = 0 Тогда
Лог.Отладка("Сущность с типом %1 не найдена", ОбъектМодели.ТипСущности());
Возврат НайденныеСтроки;
@@ -170,7 +265,7 @@
Запрос = Коннектор.КонструкторКоннектора.НовыйЗапрос(Коннектор.Соединение);
Запрос.Текст = ТекстЗапроса;
Запрос.УстановитьПараметр("Идентификатор", ОбъектМодели.ПолучитьЗначениеИдентификатора(Сущность));
- Запрос.ВыполнитьКоманду();
+ ВыполнитьСНаблюдением(Запрос, "DELETE", ИмяТаблицы, Ложь);
КонецПроцедуры
@@ -245,8 +340,8 @@
Запрос = Коннектор.КонструкторКоннектора.НовыйЗапрос(Коннектор.Соединение);
Запрос.Текст = ТекстЗапроса;
-
- Запрос.ВыполнитьКоманду();
+
+ ВыполнитьСНаблюдением(Запрос, "CREATE TABLE", ИмяТаблицы, Ложь);
КонецПроцедуры
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.os"
index 1a8aa907..4c316b6e 100644
--- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.os"
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.os"
@@ -17,6 +17,9 @@
Перем пРазмерПула;
// ПулСоединенийСБД - Пул соединений источника.
Перем пПул;
+// НаблюдателиИсточникаДанных - Реестр наблюдателей: общий у источника, его пула и всех менеджеров,
+// созданных из него.
+Перем пНаблюдатели;
// АтомарноеБулево - Закрывать источник по завершению потока, в котором он создан. Атомарное:
// сеттер зовут из одного потока, обработчик завершения читает в другом.
Перем пАвтоЗакрытие;
@@ -69,6 +72,9 @@
// проверяются при создании: тип без интерфейса АбстрактныйКоннектор и размер, не являющийся
// неотрицательным числом, отвергаются с ошибкой.
//
+// Наблюдатели регистрируются на источнике: реестр общий у источника, его пула и всех менеджеров,
+// созданных из источника.
+//
// Параметры:
// Имя - Строка - Имя источника данных.
// ТипКоннектора - Тип - Тип класса, реализующего интерфейс АбстрактныйКоннектор.
@@ -107,7 +113,10 @@
пПараметрыКоннектора = Новый ФиксированныйМассив(ПараметрыКоннектора);
КонецЕсли;
+ пНаблюдатели = Новый НаблюдателиИсточникаДанных(пТипКоннектора);
+
пПул = Новый ПулСоединенийСБД(пРазмерПула, пТипКоннектора, пСтрокаСоединения, пПараметрыКоннектора);
+ пПул.УстановитьНаблюдателей(пНаблюдатели);
пАвтоЗакрытие = Новый АтомарноеБулево(Истина);
пЗакрыт = Новый АтомарноеБулево(Ложь);
@@ -136,6 +145,46 @@
КонецПроцедуры
+// Регистрирует наблюдателя источника данных.
+//
+// Наблюдатель получает события всех операций, запросов, соединений и транзакций каждого
+// менеджера, созданного из этого источника: самого менеджера, его хранилищ сущностей и
+// Active Record. Реестр общий у источника и его менеджеров, поэтому регистрировать можно
+// и после создания менеджера. См. интерфейс НаблюдательИсточникаДанных и класс
+// СобытиеИсточникаДанных.
+//
+// Параметры:
+// Наблюдатель - Произвольный - Объект с аннотацией &Реализует("НаблюдательИсточникаДанных") на конструкторе,
+// реализующий этот интерфейс. Неполная реализация отвергается при регистрации.
+//
+// Пример:
+// Источник.ДобавитьНаблюдателя(Новый МойНаблюдатель);
+//
+Процедура ДобавитьНаблюдателя(Наблюдатель) Экспорт
+ пНаблюдатели.Добавить(Наблюдатель);
+КонецПроцедуры
+
+// Снимает наблюдателя источника данных с регистрации.
+//
+// Параметры:
+// Наблюдатель - Произвольный - Ранее зарегистрированный наблюдатель. Незарегистрированный пропускается.
+//
+Процедура УдалитьНаблюдателя(Наблюдатель) Экспорт
+ пНаблюдатели.Удалить(Наблюдатель);
+КонецПроцедуры
+
+// BSLLS:UsingServiceTag-off
+// @internal
+// BSLLS:UsingServiceTag-on
+// Реестр наблюдателей источника: источник передает его своему пулу соединений.
+//
+// Возвращаемое значение:
+// НаблюдателиИсточникаДанных - Реестр наблюдателей.
+//
+Функция Наблюдатели() Экспорт
+ Возврат пНаблюдатели;
+КонецФункции
+
// Имя источника данных.
//
// Возвращаемое значение:
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os"
index 4744d15b..8b5c9d4b 100644
--- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os"
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os"
@@ -11,14 +11,40 @@
Перем Блокировка;
// Рефлектор - Читает и заполняет поля сущностей по именам колонок модели.
Перем Рефлектор;
+// НаблюдателиИсточникаДанных - Наблюдатели пула, которым сообщается о каждой операции с таблицами.
+// Неопределено, пока коннектор работает вне пула.
+Перем Наблюдатели;
+// Строка - Строка подключения, с которой открыт коннектор.
+Перем СтрокаСоединенияКоннектора;
// Конструктор объекта АбстрактныйКоннектор.
//
+&Реализует("АбстрактныйКоннектор")
+&Реализует("НаблюдаемыйКоннектор")
Процедура ПриСозданииОбъекта()
Открыт = Ложь;
Рефлектор = Новый Рефлектор;
+ СтрокаСоединенияКоннектора = "";
КонецПроцедуры
+// Передает реестр наблюдателей, которым коннектор сообщает об операциях с таблицами.
+//
+// Параметры:
+// ПНаблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей пула.
+//
+Процедура УстановитьНаблюдателей(ПНаблюдатели) Экспорт
+ Наблюдатели = ПНаблюдатели;
+КонецПроцедуры
+
+// Описывает соединение для атрибутов событий.
+//
+// Возвращаемое значение:
+// Структура - Система "inmemory", пустой адрес, нулевой порт и БазаДанных - строка подключения.
+//
+Функция ОписаниеСоединения() Экспорт
+ Возврат СтрокиСоединения.ОписаниеБезСервера("inmemory", СтрокаСоединенияКоннектора);
+КонецФункции
+
// Открыть соединение с БД.
//
// Параметры:
@@ -26,6 +52,7 @@
// ПараметрыКоннектора - Массив - Дополнительные параметры инициализации коннектора.
//
Процедура Открыть(СтрокаСоединения, ПараметрыКоннектора) Экспорт // BSLLS:UnusedParameters-off
+ СтрокаСоединенияКоннектора = СтрокаСоединения;
КешТаблиц = ХранилищеВПамяти.КешТаблиц(СтрокаСоединения);
СчетчикиИдентификаторов = ХранилищеВПамяти.СчетчикиИдентификаторов(СтрокаСоединения);
Блокировка = ХранилищеВПамяти.Блокировка(СтрокаСоединения);
@@ -73,16 +100,55 @@
//
Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт
+ Событие = НачатьЗапрос("CREATE TABLE", ОбъектМодели);
Блокировка.Запись().Заблокировать();
Попытка
СоздатьТаблицу(ОбъектМодели);
Исключение
Блокировка.Запись().Разблокировать();
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
Блокировка.Запись().Разблокировать();
+ ЗавершитьЗапрос(Событие);
+
+КонецПроцедуры
+
+// Начинает событие запроса для наблюдателей пула.
+//
+// Параметры:
+// ОперацияСУБД - Строка - Операция события.
+// ОбъектМодели - ОбъектМодели - Модель таблицы.
+//
+// Возвращаемое значение:
+// СобытиеИсточникаДанных, Неопределено - Событие; Неопределено вне пула или без наблюдателей.
+//
+Функция НачатьЗапрос(Знач ОперацияСУБД, ОбъектМодели)
+
+ Если Наблюдатели = Неопределено Тогда
+ Возврат Неопределено;
+ КонецЕсли;
+
+ Возврат Наблюдатели.НачатьЗапрос(ОперацияСУБД, ОбъектМодели.ИмяТаблицы());
+
+КонецФункции
+
+// Завершает событие запроса для наблюдателей пула.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных, Неопределено - Событие из НачатьЗапрос.
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка операции.
+// КоличествоСтрок - Число, Неопределено - Число прочитанных строк.
+//
+Процедура ЗавершитьЗапрос(Событие, Знач ИнформацияОбОшибке = Неопределено, Знач КоличествоСтрок = Неопределено)
+
+ Если Событие = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ Наблюдатели.ЗавершитьЗапрос(Событие, ИнформацияОбОшибке, КоличествоСтрок);
КонецПроцедуры
@@ -115,16 +181,19 @@
//
Процедура Сохранить(ОбъектМодели, Сущность) Экспорт
+ Событие = НачатьЗапрос("INSERT", ОбъектМодели);
Блокировка.Запись().Заблокировать();
Попытка
ЗаписатьСущность(ОбъектМодели, Сущность);
Исключение
Блокировка.Запись().Разблокировать();
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
Блокировка.Запись().Разблокировать();
+ ЗавершитьЗапрос(Событие);
КонецПроцедуры
@@ -200,16 +269,19 @@
//
Процедура Удалить(ОбъектМодели, Сущность) Экспорт
+ Событие = НачатьЗапрос("DELETE", ОбъектМодели);
Блокировка.Запись().Заблокировать();
Попытка
УдалитьСущность(ОбъектМодели, Сущность);
Исключение
Блокировка.Запись().Разблокировать();
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
Блокировка.Запись().Разблокировать();
+ ЗавершитьЗапрос(Событие);
КонецПроцедуры
@@ -260,16 +332,19 @@
//
Функция НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Событие = НачатьЗапрос("SELECT", ОбъектМодели);
Блокировка.Чтение().Заблокировать();
Попытка
Результат = ПрочитатьСтрокиТаблицы(ОбъектМодели, ОпцииПоиска);
Исключение
Блокировка.Чтение().Разблокировать();
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
Блокировка.Чтение().Разблокировать();
+ ЗавершитьЗапрос(Событие, , Результат.Количество());
Возврат Результат;
@@ -406,16 +481,19 @@
//
Процедура УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Событие = НачатьЗапрос("DELETE", ОбъектМодели);
Блокировка.Запись().Заблокировать();
Попытка
УдалитьНайденныеСтроки(ОбъектМодели, ОпцииПоиска);
Исключение
Блокировка.Запись().Разблокировать();
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
Блокировка.Запись().Разблокировать();
+ ЗавершитьЗапрос(Событие);
КонецПроцедуры
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.os"
index 85d1a796..a0498f6a 100644
--- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.os"
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.os"
@@ -1,3 +1,4 @@
+#Использовать "../internal"
#Использовать "../internal/concurrent"
#Использовать asserts
@@ -12,15 +13,76 @@
Перем Лог;
+// НаблюдателиИсточникаДанных - Наблюдатели пула, которым сообщается о каждой операции с таблицами.
+// Неопределено, пока коннектор работает вне пула.
+Перем Наблюдатели;
+
// Конструктор объекта АбстрактныйКоннектор.
//
+&Реализует("АбстрактныйКоннектор")
+&Реализует("НаблюдаемыйКоннектор")
Процедура ПриСозданииОбъекта()
-
+
Открыт = Ложь;
+ БазовыйКаталог = "";
Лог = Логирование.ПолучитьЛог("oscript.lib.entity.connector.json");
КонецПроцедуры
+// Передает реестр наблюдателей, которым коннектор сообщает об операциях с таблицами.
+//
+// Параметры:
+// ПНаблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей пула.
+//
+Процедура УстановитьНаблюдателей(ПНаблюдатели) Экспорт
+ Наблюдатели = ПНаблюдатели;
+КонецПроцедуры
+
+// Описывает соединение для атрибутов событий.
+//
+// Возвращаемое значение:
+// Структура - Система "json", пустой адрес, нулевой порт и БазаДанных - каталог файлов таблиц.
+//
+Функция ОписаниеСоединения() Экспорт
+ Возврат СтрокиСоединения.ОписаниеБезСервера("json", БазовыйКаталог);
+КонецФункции
+
+// Начинает событие запроса для наблюдателей пула.
+//
+// Параметры:
+// ОперацияСУБД - Строка - Операция события.
+// ОбъектМодели - ОбъектМодели - Модель таблицы.
+//
+// Возвращаемое значение:
+// СобытиеИсточникаДанных, Неопределено - Событие; Неопределено вне пула или без наблюдателей.
+//
+Функция НачатьЗапрос(Знач ОперацияСУБД, ОбъектМодели)
+
+ Если Наблюдатели = Неопределено Тогда
+ Возврат Неопределено;
+ КонецЕсли;
+
+ Возврат Наблюдатели.НачатьЗапрос(ОперацияСУБД, ОбъектМодели.ИмяТаблицы());
+
+КонецФункции
+
+// Завершает событие запроса для наблюдателей пула.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных, Неопределено - Событие из НачатьЗапрос.
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка операции.
+// КоличествоСтрок - Число, Неопределено - Число прочитанных строк.
+//
+Процедура ЗавершитьЗапрос(Событие, Знач ИнформацияОбОшибке = Неопределено, Знач КоличествоСтрок = Неопределено)
+
+ Если Событие = Неопределено Тогда
+ Возврат;
+ КонецЕсли;
+
+ Наблюдатели.ЗавершитьЗапрос(Событие, ИнформацияОбОшибке, КоличествоСтрок);
+
+КонецПроцедуры
+
// Открыть соединение с БД.
//
// Параметры:
@@ -77,6 +139,7 @@
//
Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт
+ Событие = НачатьЗапрос("CREATE TABLE", ОбъектМодели);
ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json";
Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла).Запись();
Блокировка.Заблокировать();
@@ -92,12 +155,14 @@
Исключение
Блокировка.Разблокировать();
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
Блокировка.Разблокировать();
+ ЗавершитьЗапрос(Событие);
КонецПроцедуры
@@ -108,6 +173,7 @@
// Сущность - Произвольный - Объект (экземпляр класса, зарегистрированного в модели) для сохранения в БД.
//
Процедура Сохранить(ОбъектМодели, Сущность) Экспорт
+ Событие = НачатьЗапрос("INSERT", ОбъектМодели);
ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json";
Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла).Запись();
Блокировка.Заблокировать();
@@ -149,12 +215,14 @@
Исключение
Блокировка.Разблокировать();
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
Блокировка.Разблокировать();
+ ЗавершитьЗапрос(Событие);
КонецПроцедуры
@@ -165,7 +233,8 @@
// Сущность - Произвольный - Объект (экземпляр класса, зарегистрированного в модели) для удаления из БД.
//
Процедура Удалить(ОбъектМодели, Сущность) Экспорт
-
+
+ Событие = НачатьЗапрос("DELETE", ОбъектМодели);
ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json";
Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла).Запись();
Блокировка.Заблокировать();
@@ -192,12 +261,14 @@
Исключение
Блокировка.Разблокировать();
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
Блокировка.Разблокировать();
+ ЗавершитьЗапрос(Событие);
КонецПроцедуры
@@ -223,6 +294,32 @@
//
Функция НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Событие = НачатьЗапрос("SELECT", ОбъектМодели);
+
+ Попытка
+ НайденныеСтроки = ПрочитатьСтрокиТаблицы(ОбъектМодели, ОпцииПоиска);
+ Исключение
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
+ ВызватьИсключение;
+ КонецПопытки;
+
+ ЗавершитьЗапрос(Событие, , НайденныеСтроки.Количество());
+
+ Возврат НайденныеСтроки;
+
+КонецФункции
+
+// Читает строки таблицы по отбору, см. НайтиСтрокиВТаблице.
+//
+// Параметры:
+// ОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД.
+// ОпцииПоиска - ОпцииПоиска - Опции поиска.
+//
+// Возвращаемое значение:
+// Массив - Массив соответствий имя колонки - значение.
+//
+Функция ПрочитатьСтрокиТаблицы(ОбъектМодели, ОпцииПоиска)
+
НайденныеСтроки = Новый Массив;
ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json";
@@ -381,16 +478,17 @@
// В качестве "ПутьКДанным" указываются имена колонок.
//
Процедура УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
-
+
Лог.Отладка("Удаление сущностей в таблице %1", ОбъектМодели.ИмяТаблицы());
+ Событие = НачатьЗапрос("DELETE", ОбъектМодели);
ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json";
Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла).Запись();
Блокировка.Заблокировать();
Попытка
- НайденныеСтроки = НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
+ НайденныеСтроки = ПрочитатьСтрокиТаблицы(ОбъектМодели, ОпцииПоиска);
Лог.Отладка("К удалению сущностей: %1 шт.", НайденныеСтроки.Количество());
@@ -410,12 +508,14 @@
Исключение
Блокировка.Разблокировать();
+ ЗавершитьЗапрос(Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
Блокировка.Разблокировать();
+ ЗавершитьЗапрос(Событие);
КонецПроцедуры
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200PostgreSQL.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200PostgreSQL.os"
index dc899970..30e0b43c 100644
--- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200PostgreSQL.os"
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200PostgreSQL.os"
@@ -1,3 +1,5 @@
+#Использовать "../internal"
+
#Использовать logos
#Использовать strings
@@ -5,15 +7,20 @@
Перем КонструкторКоннектора Экспорт;
Перем Соединение Экспорт;
Перем КартаТипов;
+// Строка - Строка соединения, с которой открыт коннектор; нужна для описания соединения.
+Перем СтрокаСоединенияКоннектора;
Перем Лог Экспорт;
// Конструктор объекта КоннекторPostgreSQL.
//
+&Реализует("АбстрактныйКоннектор")
+&Реализует("НаблюдаемыйКоннектор")
Процедура ПриСозданииОбъекта()
Лог = Логирование.ПолучитьЛог("oscript.lib.entity.connector.postgresql");
КоннекторSQL = Новый АбстрактныйКоннекторSQL(ЭтотОбъект, Лог);
+ СтрокаСоединенияКоннектора = "";
КонструкторКоннектора = ПолучитьКонструкторКоннектора();
Соединение = КонструкторКоннектора.НовыйСоединение();
@@ -28,9 +35,37 @@
// ПараметрыКоннектора - Массив - Дополнительные параметры инициализации коннектора.
//
Процедура Открыть(СтрокаСоединения, ПараметрыКоннектора) Экспорт
+ СтрокаСоединенияКоннектора = СтрокаСоединения;
КонструкторКоннектора.Открыть(Соединение, СтрокаСоединения);
КонецПроцедуры
+// Передает реестр наблюдателей, которым коннектор сообщает о запросах.
+//
+// Параметры:
+// Наблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей пула.
+//
+Процедура УстановитьНаблюдателей(Наблюдатели) Экспорт
+ КоннекторSQL.УстановитьНаблюдателей(Наблюдатели);
+КонецПроцедуры
+
+// Описывает соединение для атрибутов событий.
+//
+// Возвращаемое значение:
+// Структура - Система "postgresql", Адрес и Порт сервера, БазаДанных из строки соединения. Без пароля.
+//
+Функция ОписаниеСоединения() Экспорт
+ Возврат СтрокиСоединения.ОписаниеСервера("postgresql", СтрокаСоединенияКоннектора, ПортПоУмолчанию());
+КонецФункции
+
+// Порт сервера PostgreSQL, если в строке соединения он не указан.
+//
+// Возвращаемое значение:
+// Число - Стандартный порт PostgreSQL.
+//
+Функция ПортПоУмолчанию()
+ Возврат 5432; // BSLLS:MagicNumber-off
+КонецФункции
+
// Закрыть соединение с БД.
//
Процедура Закрыть() Экспорт
@@ -50,25 +85,31 @@
// Начинает новую транзакцию в БД.
//
Процедура НачатьТранзакцию() Экспорт
- Запрос = КонструкторКоннектора.НовыйЗапрос(Соединение);
- Запрос.Текст = "START TRANSACTION;";
- Запрос.ВыполнитьКоманду();
+ ВыполнитьКоманду("BEGIN", "START TRANSACTION;");
КонецПроцедуры
// Фиксирует открытую транзакцию в БД.
//
Процедура ЗафиксироватьТранзакцию() Экспорт
- Запрос = КонструкторКоннектора.НовыйЗапрос(Соединение);
- Запрос.Текст = "COMMIT;";
- Запрос.ВыполнитьКоманду();
+ ВыполнитьКоманду("COMMIT", "COMMIT;");
КонецПроцедуры
// Отменяет открытую транзакцию в БД.
//
Процедура ОтменитьТранзакцию() Экспорт
+ ВыполнитьКоманду("ROLLBACK", "ROLLBACK;");
+КонецПроцедуры
+
+// Выполняет команду без таблицы, сообщая о ней наблюдателям.
+//
+// Параметры:
+// ОперацияСУБД - Строка - Операция для события запроса.
+// ТекстКоманды - Строка - Текст команды.
+//
+Процедура ВыполнитьКоманду(Знач ОперацияСУБД, Знач ТекстКоманды)
Запрос = КонструкторКоннектора.НовыйЗапрос(Соединение);
- Запрос.Текст = "ROLLBACK;";
- Запрос.ВыполнитьКоманду();
+ Запрос.Текст = ТекстКоманды;
+ КоннекторSQL.ВыполнитьСНаблюдением(Запрос, ОперацияСУБД, "", Ложь);
КонецПроцедуры
@@ -206,7 +247,7 @@
// поэтому внешняя блокировка тут не нужна.
Если СоздаватьНовыйИдентификатор Тогда
- РезультатВставки = Запрос.Выполнить().Выгрузить();
+ РезультатВставки = КоннекторSQL.ВыполнитьСНаблюдением(Запрос, "INSERT", ИмяТаблицы, Истина);
ОбъектМодели.УстановитьЗначениеКолонкиВПоле(
Сущность,
@@ -215,7 +256,7 @@
);
Иначе
- Запрос.ВыполнитьКоманду();
+ КоннекторSQL.ВыполнитьСНаблюдением(Запрос, "INSERT", ИмяТаблицы, Ложь);
КонецЕсли;
КонецПроцедуры
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os"
index 729eda82..6a22b4bd 100644
--- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os"
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os"
@@ -1,3 +1,5 @@
+#Использовать "../internal"
+
#Использовать asserts
#Использовать logos
#Использовать reflector
@@ -7,15 +9,20 @@
Перем КонструкторКоннектора Экспорт;
Перем Соединение Экспорт;
Перем КартаТипов;
+// Строка - Строка соединения, с которой открыт коннектор; нужна для описания соединения.
+Перем СтрокаСоединенияКоннектора;
Перем Лог;
// Конструктор объекта КоннекторSQLite.
//
+&Реализует("АбстрактныйКоннектор")
+&Реализует("НаблюдаемыйКоннектор")
Процедура ПриСозданииОбъекта()
Лог = Логирование.ПолучитьЛог("oscript.lib.entity.connector.sqlite");
КоннекторSQL = Новый АбстрактныйКоннекторSQL(ЭтотОбъект, Лог);
+ СтрокаСоединенияКоннектора = "";
КонструкторКоннектора = ПолучитьКонструкторКоннектора();
Соединение = КонструкторКоннектора.НовыйСоединение();
@@ -30,9 +37,35 @@
// ПараметрыКоннектора - Массив - Дополнительные параметры инициализации коннектора.
//
Процедура Открыть(СтрокаСоединения, ПараметрыКоннектора) Экспорт
+ СтрокаСоединенияКоннектора = СтрокаСоединения;
КонструкторКоннектора.Открыть(Соединение, СтрокаСоединения);
КонецПроцедуры
+// Передает реестр наблюдателей, которым коннектор сообщает о запросах.
+//
+// Параметры:
+// Наблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей пула.
+//
+Процедура УстановитьНаблюдателей(Наблюдатели) Экспорт
+ КоннекторSQL.УстановитьНаблюдателей(Наблюдатели);
+КонецПроцедуры
+
+// Описывает соединение для атрибутов событий.
+//
+// Возвращаемое значение:
+// Структура - Система "sqlite", пустой адрес, нулевой порт и БазаДанных - путь или URI файла базы
+// без параметров запроса: в них бывают секреты.
+//
+Функция ОписаниеСоединения() Экспорт
+
+ ПараметрыСтроки = СтрокиСоединения.Параметры(СтрокаСоединенияКоннектора);
+ БазаДанных = СтрокиСоединения.Значение(
+ ПараметрыСтроки, "fulluri,data source,datasource,uri,filename", СтрокаСоединенияКоннектора);
+
+ Возврат СтрокиСоединения.ОписаниеБезСервера("sqlite", СтрокиСоединения.БезПараметровЗапроса(БазаДанных));
+
+КонецФункции
+
// Закрыть соединение с БД.
//
Процедура Закрыть() Экспорт
@@ -52,25 +85,31 @@
// Начинает новую транзакцию в БД.
//
Процедура НачатьТранзакцию() Экспорт
- Запрос = КонструкторКоннектора.НовыйЗапрос(Соединение);
- Запрос.Текст = "BEGIN TRANSACTION;";
- Запрос.ВыполнитьКоманду();
+ ВыполнитьКоманду("BEGIN", "BEGIN TRANSACTION;");
КонецПроцедуры
// Фиксирует открытую транзакцию в БД.
//
Процедура ЗафиксироватьТранзакцию() Экспорт
- Запрос = КонструкторКоннектора.НовыйЗапрос(Соединение);
- Запрос.Текст = "COMMIT TRANSACTION;";
- Запрос.ВыполнитьКоманду();
+ ВыполнитьКоманду("COMMIT", "COMMIT TRANSACTION;");
КонецПроцедуры
// Отменяет открытую транзакцию в БД.
//
Процедура ОтменитьТранзакцию() Экспорт
+ ВыполнитьКоманду("ROLLBACK", "ROLLBACK TRANSACTION;");
+КонецПроцедуры
+
+// Выполняет команду без таблицы, сообщая о ней наблюдателям.
+//
+// Параметры:
+// ОперацияСУБД - Строка - Операция для события запроса.
+// ТекстКоманды - Строка - Текст команды.
+//
+Процедура ВыполнитьКоманду(Знач ОперацияСУБД, Знач ТекстКоманды)
Запрос = КонструкторКоннектора.НовыйЗапрос(Соединение);
- Запрос.Текст = "ROLLBACK TRANSACTION;";
- Запрос.ВыполнитьКоманду();
+ Запрос.Текст = ТекстКоманды;
+ КоннекторSQL.ВыполнитьСНаблюдением(Запрос, ОперацияСУБД, "", Ложь);
КонецПроцедуры
// Создает таблицу в БД по данным модели.
@@ -145,7 +184,7 @@
Лог.Отладка("Сохранение сущности с типом %1:%2%3", ОбъектМодели.ТипСущности(), Символы.ПС, ТекстЗапроса);
Запрос.Текст = ТекстЗапроса;
- Запрос.ВыполнитьКоманду();
+ КоннекторSQL.ВыполнитьСНаблюдением(Запрос, "INSERT", ИмяТаблицы, Ложь);
// Идентификатор объекта перезаписывается только тогда, когда его выдала СУБД. Если он был
// задан вызывающим кодом, объект уже несет верное значение.
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os"
index 5c5d7598..33777345 100644
--- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os"
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os"
@@ -34,14 +34,15 @@
// Конструктор объекта МенеджерСущностей.
//
// Менеджер создается из источника данных и берет соединения из его пула: все менеджеры одного
-// источника делят соединения и транзакции потоков. Параметры соединения задает источник, а его
-// временем жизни управляет тот, кто источник создал.
+// источника делят соединения, транзакции потоков и наблюдателей. Параметры соединения задает
+// источник, а его временем жизни управляет тот, кто источник создал.
//
// Параметры:
// ИсточникДанных - ИсточникДанных - Источник данных, из которого создается менеджер.
//
// Пример:
// Источник = Новый ИсточникДанных("Основной", Тип("КоннекторSQLite"), СтрокаСоединения);
+// Источник.ДобавитьНаблюдателя(Новый МойНаблюдатель);
// МенеджерСущностей = Новый МенеджерСущностей(Источник);
//
Процедура ПриСозданииОбъекта(Знач ИсточникДанных)
@@ -92,16 +93,18 @@
ПроверитьЧтоМенеджерНеЗакрыт();
Пул = Источник.ПулСоединений();
- Соединение = Пул.Занять();
+ Наблюдатели = Пул.Наблюдатели();
+ Событие = Наблюдатели.НачатьОперацию("Инициализировать");
+ Соединение = ОперацииИсточникаДанных.ЗанятьДляОперации(Пул, Событие);
Попытка
СоздатьТаблицы(Соединение.ПолучитьКоннектор());
Исключение
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие, ИнформацияОбОшибке());
ВызватьИсключение;
КонецПопытки;
- Пул.Освободить();
+ ОперацииИсточникаДанных.ОсвободитьИЗавершитьОперацию(Пул, Наблюдатели, Событие);
КонецПроцедуры
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.os"
new file mode 100644
index 00000000..a6dd57d7
--- /dev/null
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200.os"
@@ -0,0 +1,41 @@
+// Интерфейс коннектора, умеющего сообщать наблюдателям источника данных о запросах к СУБД.
+//
+// Дополняет АбстрактныйКоннектор, а не заменяет его: коннектор без этого интерфейса работает
+// как раньше, только наблюдатели не получают событий уровня запроса и описания соединения.
+// Встроенные коннекторы объявляют оба интерфейса аннотацией &Реализует на конструкторе.
+//
+// Пул соединений после открытия коннектора передает ему реестр наблюдателей и запрашивает
+// описание соединения. Описание никогда не содержит секретов: пароль из строки соединения
+// в него не попадает.
+//
+// Пример:
+// &Реализует("АбстрактныйКоннектор")
+// &Реализует("НаблюдаемыйКоннектор")
+// Процедура ПриСозданииОбъекта()
+// КонецПроцедуры
+//
+&Интерфейс
+Процедура ПриСозданииОбъекта()
+КонецПроцедуры
+
+// Передает коннектору реестр наблюдателей, которым он сообщает о запросах.
+//
+// Параметры:
+// Наблюдатели - НаблюдателиИсточникаДанных - Реестр наблюдателей пула. Коннектор создает события через
+// НачатьЗапрос и завершает через ЗавершитьЗапрос; при пустом реестре события не создаются.
+//
+Процедура УстановитьНаблюдателей(Наблюдатели) Экспорт // BSLLS:UnusedParameters-off
+КонецПроцедуры
+
+// Описывает соединение для атрибутов событий.
+//
+// Возвращаемое значение:
+// Структура - Описание соединения без секретов:
+// * Система - Строка - СУБД: postgresql, sqlite, inmemory, json.
+// * Адрес - Строка - Адрес сервера; пустая строка, если сервера нет.
+// * Порт - Число - Порт сервера; 0, если сервера нет.
+// * БазаДанных - Строка - Имя базы, путь к файлу или каталогу.
+//
+Функция ОписаниеСоединения() Экспорт
+ Возврат Неопределено;
+КонецФункции
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
new file mode 100644
index 00000000..1877f373
--- /dev/null
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
@@ -0,0 +1,51 @@
+// Интерфейс наблюдателя источника данных.
+//
+// Наблюдатель получает события источника данных, с которым работает менеджер сущностей:
+// операции (Сохранить, Получить, ПолучитьОдно, Удалить, Инициализировать, ВыполнитьСКоннектором,
+// ВычислитьСКоннектором), запросы к СУБД, захват и освобождение соединений, транзакции.
+// Каждое событие приходит дважды: до выполнения и после, одним и тем же объектом
+// СобытиеИсточникаДанных.
+//
+// Реализация объявляет интерфейс аннотацией &Реализует("НаблюдательИсточникаДанных") на конструкторе
+// и регистрируется на источнике данных методом ДобавитьНаблюдателя; менеджер, созданный из
+// источника, отдает ей события. Регистрация проверяет реализацию через ВалидаторРеализации
+// библиотеки extends, поэтому неполная реализация отвергается сразу.
+//
+// Наблюдатель вызывается из разных потоков исполнения одновременно и не должен держать
+// несинхронизированного общего состояния: все, что нужно между фазами, кладется в событие
+// методом УстановитьСостояние. Исключение наблюдателя пишется в лог и операцию не прерывает.
+//
+// Пример:
+// &Реализует("НаблюдательИсточникаДанных")
+// Процедура ПриСозданииОбъекта()
+// КонецПроцедуры
+//
+// Процедура ПередСобытием(Событие) Экспорт
+// Событие.УстановитьСостояние(ТекущаяУниверсальнаяДатаВМиллисекундах());
+// КонецПроцедуры
+//
+// Процедура ПослеСобытия(Событие) Экспорт
+// Сообщить(СтрШаблон("%1 %2: %3 мс", Событие.Вид(), Событие.Операция(), Событие.Длительность()));
+// КонецПроцедуры
+//
+&Интерфейс
+Процедура ПриСозданииОбъекта()
+КонецПроцедуры
+
+// Вызывается перед выполнением события.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных - Начатое событие. Поля результата обычно еще не заполнены;
+// у события, рассылка которого отложена, заполнены и они: см. Наблюдатели.md, раздел про
+// потоки исполнения.
+//
+Процедура ПередСобытием(Событие) Экспорт // BSLLS:UnusedParameters-off
+КонецПроцедуры
+
+// Вызывается после выполнения события, в том числе завершившегося ошибкой.
+//
+// Параметры:
+// Событие - СобытиеИсточникаДанных - Завершенное событие: заполнены Длительность, Ошибка и поля результата.
+//
+Процедура ПослеСобытия(Событие) Экспорт // BSLLS:UnusedParameters-off
+КонецПроцедуры
diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
new file mode 100644
index 00000000..8d5fbdad
--- /dev/null
+++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
@@ -0,0 +1,474 @@
+// Событие источника данных, передаваемое наблюдателям.
+//
+// Один и тот же объект приходит наблюдателю дважды: в ПередСобытием, когда заполнены только
+// описательные поля, и в ПослеСобытия, когда заполнены длительность, ошибка и поля результата.
+// Поля читаются функциями; менять их наблюдателю не следует. Для своих данных между фазами
+// у наблюдателя есть Состояние - у каждого наблюдателя свое, чужого он не видит.
+//
+// Виды событий и заполняемые поля:
+// * Операция - операция менеджера или хранилища: Операция, ТипСущности, ИмяТаблицы, Глубина;
+// после - КоличествоСтрок для чтения.
+// * Запрос - запрос к СУБД: Операция (SELECT, INSERT, DELETE, CREATE TABLE, BEGIN, COMMIT,
+// ROLLBACK), ИмяТаблицы, ТекстЗапроса (Неопределено у коннекторов без SQL); после - КоличествоСтрок.
+// * Соединение - захват и освобождение соединения пула: Операция (Занять, Освободить); после -
+// Ожидание, Открыто, ВремяОткрытия и снимок пула: Занято, Свободно, Ожидает, Предел.
+// * Транзакция - транзакция потока исполнения: после - Исход (commit, rollback, failed, abandoned).
+//
+// Методы с пометкой @internal заполняют событие изнутри библиотеки.
+
+// BSLLS:UsingServiceTag-off
+
+// Строка - Вид события, значение перечисления ВидыСобытийИсточникаДанных.
+Перем Вид;
+// Строка - Операция события, см. описание класса.
+Перем Операция;
+// Число - Вложенность операции: 0 у вызванной из прикладного кода, 1 у порожденной ею и т.д.
+Перем Глубина;
+// Тип - Тип сущности операции.
+Перем ТипСущности;
+// Строка - Имя таблицы сущности или запроса.
+Перем ИмяТаблицы;
+// Строка - Текст запроса к СУБД.
+Перем ТекстЗапроса;
+// Число - Число строк результата чтения.
+Перем КоличествоСтрок;
+// Тип - Тип коннектора пула.
+Перем ТипКоннектора;
+// Строка - Система управления базой данных: postgresql, sqlite, inmemory, json.
+Перем Система;
+// Строка - Адрес сервера СУБД.
+Перем Адрес;
+// Число - Порт сервера СУБД.
+Перем Порт;
+// Строка - Имя базы данных или путь к файлу.
+Перем БазаДанных;
+// Число - Момент начала события, универсальная дата в миллисекундах.
+Перем Начало;
+// Число - Длительность события в миллисекундах.
+Перем Длительность;
+// ИнформацияОбОшибке - Ошибка, которой завершилось событие.
+Перем Ошибка;
+// Соответствие - Данные наблюдателей между фазами: наблюдатель -> что он положил.
+Перем СостоянияНаблюдателей;
+// НаблюдательИсточникаДанных, Неопределено - Наблюдатель, которому событие рассылается сейчас.
+Перем ТекущийНаблюдатель;
+// ФиксированныйМассив из НаблюдательИсточникаДанных - Получатели события: снимок реестра на момент
+// создания, общий для обеих фаз.
+Перем Получатели;
+// Строка - Исход транзакции: commit, rollback, failed, abandoned.
+Перем Исход;
+// Число - Время ожидания соединения в очереди пула, в миллисекундах.
+Перем Ожидание;
+// Булево - Признак того, что при захвате открыто новое соединение.
+Перем Открыто;
+// Число - Время открытия нового соединения, в миллисекундах.
+Перем ВремяОткрытия;
+// Число - Соединений, занятых потоками исполнения, после события.
+Перем Занято;
+// Число - Свободных соединений в пуле после события.
+Перем Свободно;
+// Число - Потоков исполнения в очереди за соединением после события.
+Перем Ожидает;
+// Число - Размер пула.
+Перем Предел;
+
+// Создает начатое событие.
+//
+// Создается библиотекой, напрямую конструировать не следует.
+//
+// Параметры:
+// ПВид - Строка - Вид события, значение перечисления ВидыСобытийИсточникаДанных.
+// ПОперация - Строка - Операция события.
+// ПГлубина - Число - Вложенность операции.
+// ПНачало - Число - Момент начала события в миллисекундах, если работа началась раньше создания
+// события. Неопределено - началом считается создание.
+//
+Процедура ПриСозданииОбъекта(Знач ПВид, Знач ПОперация, Знач ПГлубина = 0, Знач ПНачало = Неопределено)
+
+ Вид = ПВид;
+ Операция = ПОперация;
+ Глубина = ПГлубина;
+ Начало = ?(ПНачало = Неопределено, ТекущаяУниверсальнаяДатаВМиллисекундах(), ПНачало);
+ Порт = 0;
+ Адрес = "";
+ БазаДанных = "";
+ Система = "";
+ ИмяТаблицы = "";
+ СостоянияНаблюдателей = Новый Соответствие;
+ Получатели = Новый ФиксированныйМассив(Новый Массив);
+
+КонецПроцедуры
+
+// Вид события.
+//
+// Возвращаемое значение:
+// Строка - Значение перечисления ВидыСобытийИсточникаДанных: Операция, Запрос, Соединение или Транзакция.
+//
+Функция Вид() Экспорт
+ Возврат Вид;
+КонецФункции
+
+// Операция события.
+//
+// Возвращаемое значение:
+// Строка - Имя операции, см. описание класса.
+//
+Функция Операция() Экспорт
+ Возврат Операция;
+КонецФункции
+
+// Вложенность операции.
+//
+// Возвращаемое значение:
+// Число - 0 у операции из прикладного кода, 1 у порожденной ею разыменованием ссылки и т.д.
+//
+Функция Глубина() Экспорт
+ Возврат Глубина;
+КонецФункции
+
+// Тип сущности операции.
+//
+// Возвращаемое значение:
+// Тип - Тип сущности.
+// Неопределено - Операция не привязана к сущности.
+//
+Функция ТипСущности() Экспорт
+ Возврат ТипСущности;
+КонецФункции
+
+// Имя таблицы сущности или запроса.
+//
+// Возвращаемое значение:
+// Строка - Имя таблицы; пустая строка, если таблицы нет.
+//
+Функция ИмяТаблицы() Экспорт
+ Возврат ИмяТаблицы;
+КонецФункции
+
+// Текст запроса к СУБД.
+//
+// Возвращаемое значение:
+// Строка - Текст запроса с плейсхолдерами параметров.
+// Неопределено - Коннектор не формирует текст запроса.
+//
+Функция ТекстЗапроса() Экспорт
+ Возврат ТекстЗапроса;
+КонецФункции
+
+// Число строк результата чтения.
+//
+// Возвращаемое значение:
+// Число - Число строк.
+// Неопределено - Событие не читает строки или еще не завершено.
+//
+Функция КоличествоСтрок() Экспорт
+ Возврат КоличествоСтрок;
+КонецФункции
+
+// Тип коннектора пула.
+//
+// Возвращаемое значение:
+// Тип - Тип класса коннектора.
+//
+Функция ТипКоннектора() Экспорт
+ Возврат ТипКоннектора;
+КонецФункции
+
+// Система управления базой данных.
+//
+// Возвращаемое значение:
+// Строка - postgresql, sqlite, inmemory, json или имя типа коннектора в нижнем регистре.
+//
+Функция Система() Экспорт
+ Возврат Система;
+КонецФункции
+
+// Адрес сервера СУБД.
+//
+// Возвращаемое значение:
+// Строка - Адрес; пустая строка, если у СУБД нет сервера.
+//
+Функция Адрес() Экспорт
+ Возврат Адрес;
+КонецФункции
+
+// Порт сервера СУБД.
+//
+// Возвращаемое значение:
+// Число - Порт; 0, если у СУБД нет сервера.
+//
+Функция Порт() Экспорт
+ Возврат Порт;
+КонецФункции
+
+// Имя базы данных.
+//
+// Возвращаемое значение:
+// Строка - Имя базы, путь к файлу или строка подключения без секретов.
+//
+Функция БазаДанных() Экспорт
+ Возврат БазаДанных;
+КонецФункции
+
+// Момент начала события.
+//
+// Возвращаемое значение:
+// Число - Универсальная дата в миллисекундах.
+//
+Функция Начало() Экспорт
+ Возврат Начало;
+КонецФункции
+
+// Длительность события.
+//
+// Возвращаемое значение:
+// Число - Миллисекунды от начала до завершения.
+// Неопределено - Событие еще не завершено.
+//
+Функция Длительность() Экспорт
+ Возврат Длительность;
+КонецФункции
+
+// Ошибка, которой завершилось событие.
+//
+// Возвращаемое значение:
+// ИнформацияОбОшибке - Ошибка.
+// Неопределено - Событие завершилось успешно или еще не завершено.
+//
+Функция Ошибка() Экспорт
+ Возврат Ошибка;
+КонецФункции
+
+// Данные наблюдателя, сохраненные между фазами.
+//
+// У каждого наблюдателя свое состояние: возвращается то, что в ПередСобытием положил
+// наблюдатель, которому событие рассылается сейчас.
+//
+// Возвращаемое значение:
+// Произвольный - Что положил наблюдатель; Неопределено, если ничего.
+//
+Функция Состояние() Экспорт
+ Возврат СостоянияНаблюдателей.Получить(ТекущийНаблюдатель);
+КонецФункции
+
+// Сохраняет данные наблюдателя до фазы ПослеСобытия.
+//
+// Состояние принадлежит наблюдателю, который его положил: другие наблюдатели того же
+// события его не видят и не затирают.
+//
+// Параметры:
+// Значение - Произвольный - Данные наблюдателя, например открытый спан.
+//
+Процедура УстановитьСостояние(Значение) Экспорт
+ СостоянияНаблюдателей.Вставить(ТекущийНаблюдатель, Значение);
+КонецПроцедуры
+
+// @internal
+// Отмечает наблюдателя, которому событие рассылается сейчас: Состояние читается и пишется для него.
+//
+// Параметры:
+// Наблюдатель - НаблюдательИсточникаДанных, Неопределено - Наблюдатель; Неопределено - рассылка окончена.
+//
+Процедура УстановитьТекущегоНаблюдателя(Знач Наблюдатель) Экспорт
+ ТекущийНаблюдатель = Наблюдатель;
+КонецПроцедуры
+
+// @internal
+// Получатели события: наблюдатели, зарегистрированные на момент его создания.
+//
+// Возвращаемое значение:
+// ФиксированныйМассив из НаблюдательИсточникаДанных - Кому событие рассылается в обеих фазах.
+//
+Функция Получатели() Экспорт
+ Возврат Получатели;
+КонецФункции
+
+// @internal
+// Фиксирует получателей события: обе фазы уходят одному и тому же снимку реестра.
+//
+// Параметры:
+// ПНаблюдатели - ФиксированныйМассив из НаблюдательИсточникаДанных - Снимок реестра.
+//
+Процедура УстановитьПолучателей(Знач ПНаблюдатели) Экспорт
+ Получатели = ПНаблюдатели;
+КонецПроцедуры
+
+// Исход транзакции.
+//
+// Возвращаемое значение:
+// Строка - commit, rollback, failed или abandoned.
+// Неопределено - Событие не о транзакции или еще не завершено.
+//
+Функция Исход() Экспорт
+ Возврат Исход;
+КонецФункции
+
+// Время ожидания соединения в очереди пула.
+//
+// Возвращаемое значение:
+// Число - Миллисекунды; 0, если соединение досталось сразу.
+// Неопределено - Событие не о захвате соединения.
+//
+Функция Ожидание() Экспорт
+ Возврат Ожидание;
+КонецФункции
+
+// Признак открытия нового соединения при захвате.
+//
+// Возвращаемое значение:
+// Булево - Истина, если пул открыл новое соединение, а не выдал свободное.
+// Неопределено - Событие не о захвате соединения.
+//
+Функция Открыто() Экспорт
+ Возврат Открыто;
+КонецФункции
+
+// Время открытия нового соединения.
+//
+// Возвращаемое значение:
+// Число - Миллисекунды; 0, если соединение не открывалось.
+// Неопределено - Событие не о захвате соединения.
+//
+Функция ВремяОткрытия() Экспорт
+ Возврат ВремяОткрытия;
+КонецФункции
+
+// Соединений, занятых потоками исполнения, после события.
+//
+// Возвращаемое значение:
+// Число - Занятые соединения.
+// Неопределено - Событие не о соединении.
+//
+Функция Занято() Экспорт
+ Возврат Занято;
+КонецФункции
+
+// Свободных соединений в пуле после события.
+//
+// Возвращаемое значение:
+// Число - Свободные соединения.
+// Неопределено - Событие не о соединении.
+//
+Функция Свободно() Экспорт
+ Возврат Свободно;
+КонецФункции
+
+// Потоков исполнения в очереди за соединением после события.
+//
+// Возвращаемое значение:
+// Число - Ожидающие.
+// Неопределено - Событие не о соединении.
+//
+Функция Ожидает() Экспорт
+ Возврат Ожидает;
+КонецФункции
+
+// Размер пула.
+//
+// Возвращаемое значение:
+// Число - Предел одновременно живущих соединений; 0 - без предела.
+// Неопределено - Событие не о соединении.
+//
+Функция Предел() Экспорт
+ Возврат Предел;
+КонецФункции
+
+// @internal
+// Привязывает событие к сущности.
+//
+// Параметры:
+// ПТипСущности - Тип - Тип сущности.
+// ПИмяТаблицы - Строка - Имя таблицы.
+//
+Процедура УстановитьСущность(Знач ПТипСущности, Знач ПИмяТаблицы) Экспорт
+ ТипСущности = ПТипСущности;
+ ИмяТаблицы = ПИмяТаблицы;
+КонецПроцедуры
+
+// @internal
+// Заполняет запрос к СУБД.
+//
+// Параметры:
+// ПИмяТаблицы - Строка - Имя таблицы запроса.
+// ПТекстЗапроса - Строка, Неопределено - Текст запроса.
+//
+Процедура УстановитьЗапрос(Знач ПИмяТаблицы, Знач ПТекстЗапроса) Экспорт
+ ИмяТаблицы = ПИмяТаблицы;
+ ТекстЗапроса = ПТекстЗапроса;
+КонецПроцедуры
+
+// @internal
+// Заполняет сведения о коннекторе и соединении.
+//
+// Параметры:
+// ПТипКоннектора - Тип - Тип коннектора.
+// Описание - Структура - Система, Адрес, Порт, БазаДанных.
+//
+Процедура УстановитьКоннектор(Знач ПТипКоннектора, Знач Описание) Экспорт
+ ТипКоннектора = ПТипКоннектора;
+ Система = Описание.Система;
+ Адрес = Описание.Адрес;
+ Порт = Описание.Порт;
+ БазаДанных = Описание.БазаДанных;
+КонецПроцедуры
+
+// @internal
+// Заполняет число строк результата.
+//
+// Параметры:
+// ПКоличествоСтрок - Число - Число строк.
+//
+Процедура УстановитьКоличествоСтрок(Знач ПКоличествоСтрок) Экспорт
+ КоличествоСтрок = ПКоличествоСтрок;
+КонецПроцедуры
+
+// @internal
+// Заполняет исход транзакции.
+//
+// Параметры:
+// ПИсход - Строка - commit, rollback, failed или abandoned.
+//
+Процедура УстановитьИсход(Знач ПИсход) Экспорт
+ Исход = ПИсход;
+КонецПроцедуры
+
+// @internal
+// Заполняет сведения о захвате соединения.
+//
+// Параметры:
+// ПОжидание - Число - Ожидание в очереди, мс.
+// ПОткрыто - Булево - Открыто ли новое соединение.
+// ПВремяОткрытия - Число - Время открытия, мс.
+//
+Процедура УстановитьДанныеЗахвата(Знач ПОжидание, Знач ПОткрыто, Знач ПВремяОткрытия) Экспорт
+ Ожидание = ПОжидание;
+ Открыто = ПОткрыто;
+ ВремяОткрытия = ПВремяОткрытия;
+КонецПроцедуры
+
+// @internal
+// Заполняет снимок пула после события.
+//
+// Параметры:
+// ПЗанято - Число - Занятые соединения.
+// ПСвободно - Число - Свободные соединения.
+// ПОжидает - Число - Ожидающие потоки исполнения.
+// ППредел - Число - Размер пула.
+//
+Процедура УстановитьСнимокПула(Знач ПЗанято, Знач ПСвободно, Знач ПОжидает, Знач ППредел) Экспорт
+ Занято = ПЗанято;
+ Свободно = ПСвободно;
+ Ожидает = ПОжидает;
+ Предел = ППредел;
+КонецПроцедуры
+
+// @internal
+// Завершает событие: фиксирует длительность и ошибку.
+//
+// Параметры:
+// ИнформацияОбОшибке - ИнформацияОбОшибке, Неопределено - Ошибка, если событие ею завершилось.
+//
+Процедура Завершить(Знач ИнформацияОбОшибке = Неопределено) Экспорт
+ Длительность = ТекущаяУниверсальнаяДатаВМиллисекундах() - Начало;
+ Ошибка = ИнформацияОбОшибке;
+КонецПроцедуры
diff --git "a/src/\320\234\320\276\320\264\321\203\320\273\320\270/\320\222\320\270\320\264\321\213\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os" "b/src/\320\234\320\276\320\264\321\203\320\273\320\270/\320\222\320\270\320\264\321\213\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
new file mode 100644
index 00000000..2b06e2cb
--- /dev/null
+++ "b/src/\320\234\320\276\320\264\321\203\320\273\320\270/\320\222\320\270\320\264\321\213\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
@@ -0,0 +1,60 @@
+// Виды событий, которые источник данных отдает наблюдателям: их возвращает функция Вид события.
+//
+// Значения отдаются функциями, а не экспортными переменными, чтобы их нельзя было изменить
+// извне. Наблюдатель сравнивает вид события с ними, а не со строковыми литералами, например
+// с ВидыСобытийИсточникаДанных.Запрос().
+
+// ФиксированныйМассив из Строка - Все виды в порядке объявления.
+Перем Виды;
+
+// Операция менеджера или хранилища: Сохранить, Получить, Удалить и другие.
+//
+// Возвращаемое значение:
+// Строка - Вид события операции.
+//
+Функция Операция() Экспорт
+ Возврат "Операция";
+КонецФункции
+
+// Запрос коннектора к СУБД: SELECT, INSERT, DELETE, CREATE TABLE, BEGIN, COMMIT, ROLLBACK.
+//
+// Возвращаемое значение:
+// Строка - Вид события запроса.
+//
+Функция Запрос() Экспорт
+ Возврат "Запрос";
+КонецФункции
+
+// Захват и освобождение соединения пула: Занять, Освободить.
+//
+// Возвращаемое значение:
+// Строка - Вид события соединения.
+//
+Функция Соединение() Экспорт
+ Возврат "Соединение";
+КонецФункции
+
+// Транзакция потока исполнения от начала до исхода.
+//
+// Возвращаемое значение:
+// Строка - Вид события транзакции.
+//
+Функция Транзакция() Экспорт
+ Возврат "Транзакция";
+КонецФункции
+
+// Все виды событий в порядке объявления.
+//
+// Возвращаемое значение:
+// ФиксированныйМассив из Строка - Операция, Запрос, Соединение, Транзакция.
+//
+Функция Виды() Экспорт
+ Возврат Виды;
+КонецФункции
+
+ПереченьВидов = Новый Массив;
+ПереченьВидов.Добавить(Операция());
+ПереченьВидов.Добавить(Запрос());
+ПереченьВидов.Добавить(Соединение());
+ПереченьВидов.Добавить(Транзакция());
+Виды = Новый ФиксированныйМассив(ПереченьВидов);
diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\235\320\260\320\261\320\273\321\216\320\264\320\265\320\275\320\270\321\217.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\235\320\260\320\261\320\273\321\216\320\264\320\265\320\275\320\270\321\217.os"
new file mode 100644
index 00000000..92937970
--- /dev/null
+++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\235\320\260\320\261\320\273\321\216\320\264\320\265\320\275\320\270\321\217.os"
@@ -0,0 +1,55 @@
+// Коннектор, реализующий только АбстрактныйКоннектор: делегирует работу КоннекторInMemory,
+// но интерфейс НаблюдаемыйКоннектор не объявляет. Проверяет, что наблюдатели получают события
+// операций и без событий запроса, а сам коннектор работает как раньше.
+
+// КоннекторInMemory - Настоящий коннектор, которому делегируются все вызовы.
+Перем Внутренний;
+
+&Реализует("АбстрактныйКоннектор")
+Процедура ПриСозданииОбъекта()
+ Внутренний = Новый КоннекторInMemory;
+КонецПроцедуры
+
+Процедура Открыть(СтрокаСоединения, ПараметрыКоннектора) Экспорт
+ Внутренний.Открыть(СтрокаСоединения, ПараметрыКоннектора);
+КонецПроцедуры
+
+Процедура Закрыть() Экспорт
+ Внутренний.Закрыть();
+КонецПроцедуры
+
+Функция Открыт() Экспорт
+ Возврат Внутренний.Открыт();
+КонецФункции
+
+Процедура НачатьТранзакцию() Экспорт
+ Внутренний.НачатьТранзакцию();
+КонецПроцедуры
+
+Процедура ЗафиксироватьТранзакцию() Экспорт
+ Внутренний.ЗафиксироватьТранзакцию();
+КонецПроцедуры
+
+Процедура ОтменитьТранзакцию() Экспорт
+ Внутренний.ОтменитьТранзакцию();
+КонецПроцедуры
+
+Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт
+ Внутренний.ИнициализироватьТаблицу(ОбъектМодели);
+КонецПроцедуры
+
+Процедура Сохранить(ОбъектМодели, Сущность) Экспорт
+ Внутренний.Сохранить(ОбъектМодели, Сущность);
+КонецПроцедуры
+
+Процедура Удалить(ОбъектМодели, Сущность) Экспорт
+ Внутренний.Удалить(ОбъектМодели, Сущность);
+КонецПроцедуры
+
+Функция НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Возврат Внутренний.НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
+КонецФункции
+
+Процедура УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Внутренний.УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
+КонецПроцедуры
diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\235\320\265\320\277\320\276\320\273\320\275\320\276\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\235\320\265\320\277\320\276\320\273\320\275\320\276\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271.os"
new file mode 100644
index 00000000..edae5cd0
--- /dev/null
+++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\235\320\265\320\277\320\276\320\273\320\275\320\276\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271.os"
@@ -0,0 +1,61 @@
+// Коннектор, объявивший НаблюдаемыйКоннектор, но не реализовавший его целиком: нет
+// ОписаниеСоединения. Нужен для проверки, что валидатор отвергает такой коннектор при каждом
+// открытии соединения, а не только при первом.
+//
+// АбстрактныйКоннектор реализован полностью: неполный не пережил бы даже конструктор источника.
+
+// КоннекторInMemory - Настоящий коннектор, которому делегируются все вызовы.
+Перем Внутренний;
+
+&Реализует("АбстрактныйКоннектор")
+&Реализует("НаблюдаемыйКоннектор")
+Процедура ПриСозданииОбъекта()
+ Внутренний = Новый КоннекторInMemory;
+КонецПроцедуры
+
+Процедура Открыть(СтрокаСоединения, ПараметрыКоннектора) Экспорт
+ Внутренний.Открыть(СтрокаСоединения, ПараметрыКоннектора);
+КонецПроцедуры
+
+Процедура Закрыть() Экспорт
+ Внутренний.Закрыть();
+КонецПроцедуры
+
+Функция Открыт() Экспорт
+ Возврат Внутренний.Открыт();
+КонецФункции
+
+Процедура УстановитьНаблюдателей(Наблюдатели) Экспорт // BSLLS:UnusedParameters-off
+КонецПроцедуры
+
+Процедура НачатьТранзакцию() Экспорт
+ Внутренний.НачатьТранзакцию();
+КонецПроцедуры
+
+Процедура ЗафиксироватьТранзакцию() Экспорт
+ Внутренний.ЗафиксироватьТранзакцию();
+КонецПроцедуры
+
+Процедура ОтменитьТранзакцию() Экспорт
+ Внутренний.ОтменитьТранзакцию();
+КонецПроцедуры
+
+Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт
+ Внутренний.ИнициализироватьТаблицу(ОбъектМодели);
+КонецПроцедуры
+
+Процедура Сохранить(ОбъектМодели, Сущность) Экспорт
+ Внутренний.Сохранить(ОбъектМодели, Сущность);
+КонецПроцедуры
+
+Процедура Удалить(ОбъектМодели, Сущность) Экспорт
+ Внутренний.Удалить(ОбъектМодели, Сущность);
+КонецПроцедуры
+
+Функция НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Возврат Внутренний.НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
+КонецФункции
+
+Процедура УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Внутренний.УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
+КонецПроцедуры
diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\233\320\276\320\274\320\260\320\275\321\213\320\274\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\233\320\276\320\274\320\260\320\275\321\213\320\274\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274.os"
new file mode 100644
index 00000000..622daf55
--- /dev/null
+++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\233\320\276\320\274\320\260\320\275\321\213\320\274\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274.os"
@@ -0,0 +1,81 @@
+// Коннектор, объявивший НаблюдаемыйКоннектор, у которого ОписаниеСоединения завершается ошибкой.
+// Проверяет, что открытый коннектор при ошибке подключения наблюдателей закрывается, а не течет.
+// Признак закрытия кладется в данные текущего потока исполнения под ключом КлючЗакрыт.
+
+// КоннекторInMemory - Настоящий коннектор, которому делегируются все вызовы.
+Перем Внутренний;
+
+&Реализует("АбстрактныйКоннектор")
+&Реализует("НаблюдаемыйКоннектор")
+Процедура ПриСозданииОбъекта()
+ Внутренний = Новый КоннекторInMemory;
+КонецПроцедуры
+
+// Ключ данных потока исполнения, под которым лежит признак закрытия коннектора.
+//
+// Возвращаемое значение:
+// Строка - Ключ.
+//
+Функция КлючЗакрыт() Экспорт
+ Возврат "КоннекторСЛоманымОписанием.Закрыт";
+КонецФункции
+
+Процедура Открыть(СтрокаСоединения, ПараметрыКоннектора) Экспорт
+ Внутренний.Открыть(СтрокаСоединения, ПараметрыКоннектора);
+ ТекущийПоток().Данные.Вставить(КлючЗакрыт(), Ложь);
+КонецПроцедуры
+
+Процедура Закрыть() Экспорт
+ Внутренний.Закрыть();
+ ТекущийПоток().Данные.Вставить(КлючЗакрыт(), Истина);
+КонецПроцедуры
+
+Функция Открыт() Экспорт
+ Возврат Внутренний.Открыт();
+КонецФункции
+
+Процедура УстановитьНаблюдателей(Наблюдатели) Экспорт // BSLLS:UnusedParameters-off
+КонецПроцедуры
+
+// BSLLS:FunctionShouldHaveReturn-off
+// Описывает соединение для атрибутов событий: в этой фикстуре всегда завершается ошибкой.
+//
+// Возвращаемое значение:
+// Структура - Не возвращается никогда: вызов завершается исключением.
+//
+Функция ОписаниеСоединения() Экспорт
+ ВызватьИсключение "Описание соединения недоступно";
+КонецФункции
+// BSLLS:FunctionShouldHaveReturn-on
+
+Процедура НачатьТранзакцию() Экспорт
+ Внутренний.НачатьТранзакцию();
+КонецПроцедуры
+
+Процедура ЗафиксироватьТранзакцию() Экспорт
+ Внутренний.ЗафиксироватьТранзакцию();
+КонецПроцедуры
+
+Процедура ОтменитьТранзакцию() Экспорт
+ Внутренний.ОтменитьТранзакцию();
+КонецПроцедуры
+
+Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт
+ Внутренний.ИнициализироватьТаблицу(ОбъектМодели);
+КонецПроцедуры
+
+Процедура Сохранить(ОбъектМодели, Сущность) Экспорт
+ Внутренний.Сохранить(ОбъектМодели, Сущность);
+КонецПроцедуры
+
+Процедура Удалить(ОбъектМодели, Сущность) Экспорт
+ Внутренний.Удалить(ОбъектМодели, Сущность);
+КонецПроцедуры
+
+Функция НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Возврат Внутренний.НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
+КонецФункции
+
+Процедура УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Внутренний.УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
+КонецПроцедуры
diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271\320\224\320\273\321\217\320\242\320\265\321\201\321\202\320\276\320\262.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271\320\224\320\273\321\217\320\242\320\265\321\201\321\202\320\276\320\262.os"
new file mode 100644
index 00000000..dd71cc39
--- /dev/null
+++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271\320\224\320\273\321\217\320\242\320\265\321\201\321\202\320\276\320\262.os"
@@ -0,0 +1,126 @@
+// Коннектор для тестов событий транзакций: данные держит в КоннекторInMemory, а транзакции
+// изображает сам, потому что InMemory их не поддерживает.
+//
+// Единственный параметр коннектора - НастройкиКоннектораТранзакций, см. описание того модуля.
+// Без параметров коннектор ведет себя как безотказная база, терпящая и вложенное начало.
+
+// КоннекторInMemory - Коннектор, которому делегируются операции с данными.
+Перем Внутренний;
+// Булево - Признак того, что начало транзакции должно падать.
+Перем ПадатьПриНачалеТранзакции;
+// Булево - Признак того, что завершение транзакции должно падать.
+Перем ПадатьПриЗавершенииТранзакции;
+// Булево - Признак того, что начало транзакции поверх начатой должно отвергаться.
+Перем ОтвергатьВложеннуюТранзакцию;
+// Число - Пауза внутри закрытия соединения в миллисекундах.
+Перем ПаузаЗакрытия;
+// СигналСобытия - Сигнал, отмечаемый в начале закрытия; Неопределено - сигналить некому.
+Перем СигналЗакрытия;
+// ПланЗакрытийКоннектора - План падений закрытия; Неопределено - закрытие не падает.
+Перем ПланЗакрытий;
+// Булево - Признак начатой транзакции. Нужен только для отказа во вложенной.
+Перем ВТранзакции;
+
+&Реализует("АбстрактныйКоннектор")
+Процедура ПриСозданииОбъекта()
+ Внутренний = Новый КоннекторInMemory;
+ ПадатьПриНачалеТранзакции = Ложь;
+ ПадатьПриЗавершенииТранзакции = Ложь;
+ ОтвергатьВложеннуюТранзакцию = Ложь;
+ ПаузаЗакрытия = 0;
+ СигналЗакрытия = Неопределено;
+ ПланЗакрытий = Неопределено;
+ ВТранзакции = Ложь;
+КонецПроцедуры
+
+Процедура Открыть(СтрокаСоединения, ПараметрыКоннектора) Экспорт
+
+ Если ПараметрыКоннектора.Количество() > 0 Тогда
+ Настройки = ПараметрыКоннектора[0];
+ ПадатьПриНачалеТранзакции = Настройки.ПадатьПриНачалеТранзакции;
+ ПадатьПриЗавершенииТранзакции = Настройки.ПадатьПриЗавершенииТранзакции;
+ ОтвергатьВложеннуюТранзакцию = Настройки.ОтвергатьВложеннуюТранзакцию;
+ ПаузаЗакрытия = Настройки.ПаузаЗакрытия;
+ СигналЗакрытия = Настройки.СигналЗакрытия;
+ ПланЗакрытий = Настройки.ПланЗакрытий;
+ КонецЕсли;
+
+ Внутренний.Открыть(СтрокаСоединения, ПараметрыКоннектора);
+
+КонецПроцедуры
+
+Процедура Закрыть() Экспорт
+
+ // Отметка до паузы: пул закрывает соединение под своей блокировкой записи, и тесту важен
+ // именно момент, когда блокировка уже занята.
+ Если СигналЗакрытия <> Неопределено Тогда
+ СигналЗакрытия.Отметить();
+ КонецЕсли;
+
+ Если ПаузаЗакрытия > 0 Тогда
+ Приостановить(ПаузаЗакрытия);
+ КонецЕсли;
+
+ Если ПланЗакрытий <> Неопределено И ПланЗакрытий.ЗакрытиеПадает() Тогда
+ ВызватьИсключение "Закрытие соединения не удалось";
+ КонецЕсли;
+
+ Внутренний.Закрыть();
+
+КонецПроцедуры
+
+Функция Открыт() Экспорт
+ Возврат Внутренний.Открыт();
+КонецФункции
+
+Процедура НачатьТранзакцию() Экспорт
+
+ Если ПадатьПриНачалеТранзакции Тогда
+ ВызватьИсключение "Начало транзакции не удалось";
+ КонецЕсли;
+
+ Если ВТранзакции И ОтвергатьВложеннуюТранзакцию Тогда
+ ВызватьИсключение "Вложенная транзакция не поддерживается";
+ КонецЕсли;
+
+ ВТранзакции = Истина;
+
+КонецПроцедуры
+
+Процедура ЗафиксироватьТранзакцию() Экспорт
+ ЗавершитьТранзакцию();
+КонецПроцедуры
+
+Процедура ОтменитьТранзакцию() Экспорт
+ ЗавершитьТранзакцию();
+КонецПроцедуры
+
+Процедура ЗавершитьТранзакцию()
+
+ Если ПадатьПриЗавершенииТранзакции Тогда
+ ВызватьИсключение "Завершение транзакции не удалось";
+ КонецЕсли;
+
+ ВТранзакции = Ложь;
+
+КонецПроцедуры
+
+Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт
+ Внутренний.ИнициализироватьТаблицу(ОбъектМодели);
+КонецПроцедуры
+
+Процедура Сохранить(ОбъектМодели, Сущность) Экспорт
+ Внутренний.Сохранить(ОбъектМодели, Сущность);
+КонецПроцедуры
+
+Процедура Удалить(ОбъектМодели, Сущность) Экспорт
+ Внутренний.Удалить(ОбъектМодели, Сущность);
+КонецПроцедуры
+
+Функция НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Возврат Внутренний.НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
+КонецФункции
+
+Процедура УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт
+ Внутренний.УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска);
+КонецПроцедуры
diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\243\320\277\321\200\320\260\320\262\320\273\321\217\320\265\320\274\320\276\320\265\320\236\321\202\320\272\321\200\321\213\321\202\320\270\320\265.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\243\320\277\321\200\320\260\320\262\320\273\321\217\320\265\320\274\320\276\320\265\320\236\321\202\320\272\321\200\321\213\321\202\320\270\320\265.os"
index 163aeffb..6db6068a 100644
--- "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\243\320\277\321\200\320\260\320\262\320\273\321\217\320\265\320\274\320\276\320\265\320\236\321\202\320\272\321\200\321\213\321\202\320\270\320\265.os"
+++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\243\320\277\321\200\320\260\320\262\320\273\321\217\320\265\320\274\320\276\320\265\320\236\321\202\320\272\321\200\321\213\321\202\320\270\320\265.os"
@@ -11,6 +11,7 @@
// НастройкиТестовогоКоннектора - Настройки, с которыми соединение открыто.
Перем Настройки;
+&Реализует("АбстрактныйКоннектор")
Процедура ПриСозданииОбъекта()
Открыт = Ложь;
Настройки = Неопределено;
diff --git "a/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\227\320\260\320\277\320\270\321\201\321\213\320\262\320\260\321\216\321\211\320\270\320\271.os" "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\227\320\260\320\277\320\270\321\201\321\213\320\262\320\260\321\216\321\211\320\270\320\271.os"
new file mode 100644
index 00000000..38d3e868
--- /dev/null
+++ "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\227\320\260\320\277\320\270\321\201\321\213\320\262\320\260\321\216\321\211\320\270\320\271.os"
@@ -0,0 +1,83 @@
+// BSLLS:ExportVariables-off
+// Наблюдатель для тестов: запоминает события обеих фаз в порядке поступления.
+//
+// Параметры конструктора:
+// ПадатьВПередСобытием - Булево - Истина, если ПередСобытием должен завершаться ошибкой.
+// Проверяет, что ошибка наблюдателя не ломает операцию.
+
+// Массив из Структура - Записи о событиях: Фаза ("До" или "После") и само Событие.
+Перем Записи Экспорт;
+// Булево - Признак того, что ПередСобытием должен падать.
+Перем ПадатьВПередСобытием;
+// БлокировкаЧтенияЗаписи - События приходят из фоновых заданий, пока тест читает записи.
+Перем Блокировка;
+
+&Реализует("НаблюдательИсточникаДанных")
+Процедура ПриСозданииОбъекта(Знач ППадатьВПередСобытием = Ложь)
+ Записи = Новый Массив;
+ ПадатьВПередСобытием = ППадатьВПередСобытием;
+ Блокировка = Новый БлокировкаЧтенияЗаписи;
+КонецПроцедуры
+
+Процедура ПередСобытием(Событие) Экспорт
+ Если ПадатьВПередСобытием Тогда
+ ВызватьИсключение "Наблюдатель сломан";
+ КонецЕсли;
+ Записать("До", Событие);
+КонецПроцедуры
+
+Процедура ПослеСобытия(Событие) Экспорт
+ Записать("После", Событие);
+КонецПроцедуры
+
+Процедура Записать(Знач Фаза, Знач Событие)
+ Блокировка.Запись().Заблокировать();
+ Записи.Добавить(Новый Структура("Фаза, Событие", Фаза, Событие));
+ Блокировка.Запись().Разблокировать();
+КонецПроцедуры
+
+// Возвращает завершенные события указанного вида.
+//
+// Параметры:
+// Вид - Строка - Вид события, значение ВидыСобытийИсточникаДанных.
+// Операция - Строка - Операция события; пустая строка - любая.
+//
+// Возвращаемое значение:
+// Массив из СобытиеИсточникаДанных - События фазы "После" в порядке завершения.
+//
+Функция Завершенные(Знач Вид, Знач Операция = "") Экспорт
+ Возврат Отобрать("После", Вид, Операция);
+КонецФункции
+
+// Возвращает начатые события указанного вида.
+//
+// Параметры:
+// Вид - Строка - Вид события, значение ВидыСобытийИсточникаДанных.
+// Операция - Строка - Операция события; пустая строка - любая.
+//
+// Возвращаемое значение:
+// Массив из СобытиеИсточникаДанных - События фазы "До" в порядке начала.
+//
+Функция Начатые(Знач Вид, Знач Операция = "") Экспорт
+ Возврат Отобрать("До", Вид, Операция);
+КонецФункции
+
+Функция Отобрать(Знач Фаза, Знач Вид, Знач Операция)
+
+ Результат = Новый Массив;
+
+ Блокировка.Чтение().Заблокировать();
+ Для Каждого Запись Из Записи Цикл
+ Если Запись.Фаза <> Фаза ИЛИ Запись.Событие.Вид() <> Вид Тогда
+ Продолжить;
+ КонецЕсли;
+ Если ЗначениеЗаполнено(Операция) И Запись.Событие.Операция() <> Операция Тогда
+ Продолжить;
+ КонецЕсли;
+ Результат.Добавить(Запись.Событие);
+ КонецЦикла;
+ Блокировка.Чтение().Разблокировать();
+
+ Возврат Результат;
+
+КонецФункции
diff --git "a/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\235\320\265\320\277\320\276\320\273\320\275\321\213\320\271.os" "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\235\320\265\320\277\320\276\320\273\320\275\321\213\320\271.os"
new file mode 100644
index 00000000..f90dffb1
--- /dev/null
+++ "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\235\320\265\320\277\320\276\320\273\320\275\321\213\320\271.os"
@@ -0,0 +1,9 @@
+// Наблюдатель, объявивший интерфейс, но не реализовавший его целиком: нет ПослеСобытия.
+// Нужен для проверки, что регистрация падает сразу, а не на первом событии.
+
+&Реализует("НаблюдательИсточникаДанных")
+Процедура ПриСозданииОбъекта()
+КонецПроцедуры
+
+Процедура ПередСобытием(Событие) Экспорт // BSLLS:UnusedParameters-off
+КонецПроцедуры
diff --git "a/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\237\320\265\321\200\320\265\320\277\320\276\320\264\320\277\320\270\321\201\321\213\320\262\320\260\321\216\321\211\320\270\320\271\321\201\321\217.os" "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\237\320\265\321\200\320\265\320\277\320\276\320\264\320\277\320\270\321\201\321\213\320\262\320\260\321\216\321\211\320\270\320\271\321\201\321\217.os"
new file mode 100644
index 00000000..dc722240
--- /dev/null
+++ "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\237\320\265\321\200\320\265\320\277\320\276\320\264\320\277\320\270\321\201\321\213\320\262\320\260\321\216\321\211\320\270\320\271\321\201\321\217.os"
@@ -0,0 +1,48 @@
+// BSLLS:ExportVariables-off
+// Наблюдатель для тестов состава получателей: в первом же ПередСобытием меняет реестр
+// источника данных - снимает себя или добавляет другого наблюдателя - и записывает события обеих фаз.
+//
+// Параметры конструктора:
+// ПИсточник - ИсточникДанных - Источник, реестр наблюдателей которого меняется.
+// ПДругой - Произвольный, Неопределено - Наблюдатель, которого добавить; Неопределено - снять себя.
+
+// Массив из СобытиеИсточникаДанных - События фазы "До" в порядке поступления.
+Перем Начатые Экспорт;
+// Массив из СобытиеИсточникаДанных - События фазы "После" в порядке поступления.
+Перем Завершенные Экспорт;
+// ИсточникДанных - Источник, реестр которого меняется.
+Перем Источник;
+// Произвольный, Неопределено - Кого добавить; Неопределено - снять себя.
+Перем Другой;
+// Булево - Реестр уже изменен: меняется он один раз, в первом событии.
+Перем РеестрИзменен;
+
+&Реализует("НаблюдательИсточникаДанных")
+Процедура ПриСозданииОбъекта(Знач ПИсточник, Знач ПДругой = Неопределено)
+ Начатые = Новый Массив;
+ Завершенные = Новый Массив;
+ Источник = ПИсточник;
+ Другой = ПДругой;
+ РеестрИзменен = Ложь;
+КонецПроцедуры
+
+Процедура ПередСобытием(Событие) Экспорт
+
+ Начатые.Добавить(Событие);
+
+ Если РеестрИзменен Тогда
+ Возврат;
+ КонецЕсли;
+ РеестрИзменен = Истина;
+
+ Если Другой = Неопределено Тогда
+ Источник.УдалитьНаблюдателя(ЭтотОбъект);
+ Иначе
+ Источник.ДобавитьНаблюдателя(Другой);
+ КонецЕсли;
+
+КонецПроцедуры
+
+Процедура ПослеСобытия(Событие) Экспорт
+ Завершенные.Добавить(Событие);
+КонецПроцедуры
diff --git "a/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\241\320\234\320\265\321\202\320\272\320\276\320\271.os" "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\241\320\234\320\265\321\202\320\272\320\276\320\271.os"
new file mode 100644
index 00000000..daecff7f
--- /dev/null
+++ "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\241\320\234\320\265\321\202\320\272\320\276\320\271.os"
@@ -0,0 +1,26 @@
+// BSLLS:ExportVariables-off
+// Наблюдатель для тестов состояния события: в ПередСобытием кладет в событие свою метку,
+// в ПослеСобытия запоминает, что из события прочитал. Два таких наблюдателя на одном менеджере
+// проверяют, что состояние у каждого свое.
+//
+// Параметры конструктора:
+// Метка - Строка - Что наблюдатель кладет в состояние события.
+
+// Массив из Произвольный - Состояния, прочитанные в ПослеСобытия, в порядке событий.
+Перем Увиденные Экспорт;
+// Строка - Метка наблюдателя.
+Перем Метка;
+
+&Реализует("НаблюдательИсточникаДанных")
+Процедура ПриСозданииОбъекта(Знач ПМетка)
+ Увиденные = Новый Массив;
+ Метка = ПМетка;
+КонецПроцедуры
+
+Процедура ПередСобытием(Событие) Экспорт
+ Событие.УстановитьСостояние(Метка);
+КонецПроцедуры
+
+Процедура ПослеСобытия(Событие) Экспорт
+ Увиденные.Добавить(Событие.Состояние());
+КонецПроцедуры
diff --git "a/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\242\320\276\321\200\320\274\320\276\320\267\321\217\321\211\320\270\320\271.os" "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\242\320\276\321\200\320\274\320\276\320\267\321\217\321\211\320\270\320\271.os"
new file mode 100644
index 00000000..bb5b284c
--- /dev/null
+++ "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\242\320\276\321\200\320\274\320\276\320\267\321\217\321\211\320\270\320\271.os"
@@ -0,0 +1,73 @@
+// Наблюдатель, который задерживается в ПослеСобытия события заданного вида.
+//
+// Нужен там, где проверяется, что чужой код не зовется из-под блокировки пула: пока такой
+// наблюдатель спит, другой поток должен свободно работать с пулом.
+//
+// BSLLS:ExportVariables-off
+
+// Массив из Структура - Записи о событиях: Фаза ("До" или "После") и само Событие.
+Перем Записи Экспорт;
+// Строка - Вид события, на завершении которого наблюдатель засыпает.
+Перем ВидСобытия;
+// Число - Пауза внутри ПослеСобытия в миллисекундах.
+Перем Пауза;
+// СигналСобытия - Сигнал, отмечаемый перед паузой: по нему тест узнает, что наблюдатель уснул.
+Перем СигналПаузы;
+// БлокировкаЧтенияЗаписи - События приходят из фоновых заданий, пока тест читает записи.
+Перем Блокировка;
+
+&Реализует("НаблюдательИсточникаДанных")
+Процедура ПриСозданииОбъекта(Знач ПВидСобытия, Знач ППауза, Знач ПСигналПаузы)
+ Записи = Новый Массив;
+ ВидСобытия = ПВидСобытия;
+ Пауза = ППауза;
+ СигналПаузы = ПСигналПаузы;
+ Блокировка = Новый БлокировкаЧтенияЗаписи;
+КонецПроцедуры
+
+Процедура ПередСобытием(Событие) Экспорт
+ Записать("До", Событие);
+КонецПроцедуры
+
+Процедура ПослеСобытия(Событие) Экспорт
+
+ Записать("После", Событие);
+
+ Если Событие.Вид() <> ВидСобытия Тогда
+ Возврат;
+ КонецЕсли;
+
+ СигналПаузы.Отметить();
+ Приостановить(Пауза);
+
+КонецПроцедуры
+
+Процедура Записать(Знач Фаза, Знач Событие)
+ Блокировка.Запись().Заблокировать();
+ Записи.Добавить(Новый Структура("Фаза, Событие", Фаза, Событие));
+ Блокировка.Запись().Разблокировать();
+КонецПроцедуры
+
+// Возвращает завершенные события указанного вида.
+//
+// Параметры:
+// Вид - Строка - Вид события.
+//
+// Возвращаемое значение:
+// Массив из СобытиеИсточникаДанных - Завершенные события этого вида в порядке поступления.
+//
+Функция Завершенные(Знач Вид) Экспорт
+
+ Найденные = Новый Массив;
+
+ Блокировка.Чтение().Заблокировать();
+ Для Каждого Запись Из Записи Цикл
+ Если Запись.Фаза = "После" И Запись.Событие.Вид() = Вид Тогда
+ Найденные.Добавить(Запись.Событие);
+ КонецЕсли;
+ КонецЦикла;
+ Блокировка.Чтение().Разблокировать();
+
+ Возврат Найденные;
+
+КонецФункции
diff --git "a/tests/fixtures/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271.os" "b/tests/fixtures/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271.os"
new file mode 100644
index 00000000..cab35741
--- /dev/null
+++ "b/tests/fixtures/\320\235\320\260\321\201\321\202\321\200\320\276\320\271\320\272\320\270\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271.os"
@@ -0,0 +1,51 @@
+// Настройки КоннекторТранзакцийДляТестов: что коннектор делает на начале и завершении транзакции.
+//
+// Пул конструирует коннекторы сам, поэтому настроить экземпляр снаружи нельзя - настройки приходят
+// тем же путем, что и всегда, через параметры коннектора. Единственным элементом массива параметров
+// идет этот объект: позиционный список флагов читать невозможно.
+//
+// Класс хранит только данные, поэтому поля открыты - как у НастройкиТестовогоКоннектора.
+//
+// BSLLS:ExportVariables-off
+
+// Булево - Истина, если начало транзакции должно завершаться ошибкой.
+Перем ПадатьПриНачалеТранзакции Экспорт;
+// Булево - Истина, если фиксация и отмена транзакции должны завершаться ошибкой.
+Перем ПадатьПриЗавершенииТранзакции Экспорт;
+// Булево - Истина, если начало транзакции поверх начатой должно отвергаться, как это делает
+// СУБД без поддержки вложенных транзакций.
+Перем ОтвергатьВложеннуюТранзакцию Экспорт;
+// Число - Пауза внутри закрытия соединения в миллисекундах. Изображает медленное закрытие,
+// которое пул делает под своей блокировкой записи.
+Перем ПаузаЗакрытия Экспорт;
+// СигналСобытия - Сигнал, который коннектор отмечает в начале закрытия. Задан - тест может
+// дождаться момента, когда блокировка пула уже занята закрывающим потоком.
+Перем СигналЗакрытия Экспорт;
+// ПланЗакрытийКоннектора - План падений закрытия: с какого по счету закрытия падать и сколько раз.
+// Задан - решает он; иначе закрытие не падает.
+Перем ПланЗакрытий Экспорт;
+
+Процедура ПриСозданииОбъекта()
+
+ ПадатьПриНачалеТранзакции = Ложь;
+ ПадатьПриЗавершенииТранзакции = Ложь;
+ ОтвергатьВложеннуюТранзакцию = Ложь;
+ ПаузаЗакрытия = 0;
+ СигналЗакрытия = Неопределено;
+ ПланЗакрытий = Неопределено;
+
+КонецПроцедуры
+
+// Собирает параметры коннектора для ИсточникДанных и ПулСоединенийСБД.
+//
+// Возвращаемое значение:
+// Массив - Параметры коннектора с этими настройками единственным элементом.
+//
+Функция ПараметрыКоннектора() Экспорт
+
+ Параметры = Новый Массив;
+ Параметры.Добавить(ЭтотОбъект);
+
+ Возврат Параметры;
+
+КонецФункции
diff --git "a/tests/fixtures/\320\241\320\270\320\263\320\275\320\260\320\273\320\241\320\276\320\261\321\213\321\202\320\270\321\217.os" "b/tests/fixtures/\320\241\320\270\320\263\320\275\320\260\320\273\320\241\320\276\320\261\321\213\321\202\320\270\321\217.os"
new file mode 100644
index 00000000..00b1f3d5
--- /dev/null
+++ "b/tests/fixtures/\320\241\320\270\320\263\320\275\320\260\320\273\320\241\320\276\320\261\321\213\321\202\320\270\321\217.os"
@@ -0,0 +1,31 @@
+// Сигнал о том, что проверяемый код дошел до нужного места.
+//
+// Тестам, где два потока должны встретиться в определенной точке, недостаточно ждать публикации
+// события: между ней и интересным местом проходит еще какой-то путь. Такой тест ждет отметку,
+// которую ставит сам код в нужной точке.
+//
+// Экземпляр приходит в фикстуры параметрами и общий у них с тестом, поэтому отметку, сделанную
+// фоновым потоком, видит основной.
+//
+
+// Булево - Признак того, что отметка поставлена.
+Перем Отмечен;
+
+Процедура ПриСозданииОбъекта()
+ Отмечен = Ложь;
+КонецПроцедуры
+
+// Ставит отметку.
+//
+Процедура Отметить() Экспорт
+ Отмечен = Истина;
+КонецПроцедуры
+
+// Определяет, поставлена ли отметка.
+//
+// Возвращаемое значение:
+// Булево - Истина, если код дошел до отмечаемого места.
+//
+Функция Отмечен() Экспорт
+ Возврат Отмечен;
+КонецФункции
diff --git "a/tests/utils/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\265\321\201\321\202\320\276\320\262\321\213\320\265\320\243\321\202\320\270\320\273\320\270\321\202\321\213.os" "b/tests/utils/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\265\321\201\321\202\320\276\320\262\321\213\320\265\320\243\321\202\320\270\320\273\320\270\321\202\321\213.os"
index 3e2b726b..e367e0a7 100644
--- "a/tests/utils/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\265\321\201\321\202\320\276\320\262\321\213\320\265\320\243\321\202\320\270\320\273\320\270\321\202\321\213.os"
+++ "b/tests/utils/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\265\321\201\321\202\320\276\320\262\321\213\320\265\320\243\321\202\320\270\320\273\320\270\321\202\321\213.os"
@@ -111,6 +111,25 @@
КонецФункции
+// Закрывает источники, созданные тестом, и очищает их перечень.
+//
+// Тест, заменивший источник новым, оставил бы прежний с открытыми соединениями до сборки мусора,
+// поэтому закрываются все созданные, а не только последний. Повторное закрытие источник переживает,
+// так что закрытый тестом сам попадает сюда безболезненно.
+//
+// Параметры:
+// Источники - Массив из ИсточникДанных - Перечень созданных тестом источников.
+//
+Процедура ЗакрытьИсточники(Источники) Экспорт
+
+ Для Каждого Источник Из Источники Цикл
+ Источник.Закрыть();
+ КонецЦикла;
+
+ Источники.Очистить();
+
+КонецПроцедуры
+
// Удаляет таблицы в базе данных менеджера сущностей.
//
// Параметры:
diff --git "a/tests/\320\222\320\270\320\264\321\213\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os" "b/tests/\320\222\320\270\320\264\321\213\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
new file mode 100644
index 00000000..236db331
--- /dev/null
+++ "b/tests/\320\222\320\270\320\264\321\213\320\241\320\276\320\261\321\213\321\202\320\270\320\271\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
@@ -0,0 +1,105 @@
+// Модуль-перечисление ВидыСобытийИсточникаДанных: значения совпадают с видами событий, которые
+// источник данных отдает наблюдателям, поэтому наблюдатель сравнивает Вид() с перечислением,
+// а не со строковыми литералами.
+//
+// BSLLS:MagicNumber-off
+
+#Использовать ".."
+#Использовать "utils"
+
+&Тест
+Процедура ПеречислениеСодержитВсеВидыВПорядкеОбъявления() Экспорт
+
+ Виды = ВидыСобытийИсточникаДанных.Виды();
+
+ Ожидаем.Что(Виды, "Перечень нельзя изменить извне").ИмеетТип("ФиксированныйМассив");
+ Ожидаем.Что(Виды).ИмеетДлину(4);
+ Ожидаем.Что(Виды[0]).Равно(ВидыСобытийИсточникаДанных.Операция());
+ Ожидаем.Что(Виды[1]).Равно(ВидыСобытийИсточникаДанных.Запрос());
+ Ожидаем.Что(Виды[2]).Равно(ВидыСобытийИсточникаДанных.Соединение());
+ Ожидаем.Что(Виды[3]).Равно(ВидыСобытийИсточникаДанных.Транзакция());
+
+КонецПроцедуры
+
+&Тест
+Процедура ЗначенияПеречисленияСовпадаютСВидамиСобытийИсточникаДанных() Экспорт
+
+ ПодключитьФикстуры();
+
+ // Операции, запросы и соединения - на InMemory; транзакции он не поддерживает,
+ // их изображает тестовый коннектор поверх него.
+ Наблюдатель = НаблюдательПослеРаботы(Тип("КоннекторInMemory"), "виды-событий");
+ НаблюдательТранзакций = НаблюдательПослеРаботы(Тип("КоннекторТранзакцийДляТестов"), "виды-событий-транзакции");
+
+ Ожидаем.Что(Наблюдатель.Завершенные(ВидыСобытийИсточникаДанных.Операция(), "Сохранить")).ИмеетДлину(1);
+ Ожидаем.Что(Наблюдатель.Завершенные(ВидыСобытийИсточникаДанных.Запрос(), "INSERT")).ИмеетДлину(1);
+ Ожидаем.Что(Наблюдатель.Завершенные(ВидыСобытийИсточникаДанных.Соединение(), "Занять")).Не_().ИмеетДлину(0);
+ Ожидаем.Что(НаблюдательТранзакций.Завершенные(ВидыСобытийИсточникаДанных.Транзакция())).ИмеетДлину(1);
+
+ // Каждое событие несет ровно одно из значений перечисления.
+ Виды = ВидыСобытийИсточникаДанных.Виды();
+ Для Каждого Запись Из Наблюдатель.Записи Цикл
+ Ожидаем.Что(Виды.Найти(Запись.Событие.Вид()), Запись.Событие.Вид()).Не_().Равно(Неопределено);
+ КонецЦикла;
+
+КонецПроцедуры
+
+// Служебные
+
+// Прогоняет через менеджер на указанном коннекторе сохранение внутри транзакции и возвращает
+// наблюдателя с записанными событиями.
+//
+// Параметры:
+// ТипКоннектора - Тип - Тип коннектора тестовой базы.
+// СтрокаСоединения - Строка - Строка соединения с тестовой базой.
+//
+// Возвращаемое значение:
+// НаблюдательЗаписывающий - Наблюдатель с записанными событиями.
+//
+Функция НаблюдательПослеРаботы(Знач ТипКоннектора, Знач СтрокаСоединения)
+
+ ХранилищеВПамяти.Очистить(СтрокаСоединения);
+
+ Источник = Новый ИсточникДанных("виды", ТипКоннектора, СтрокаСоединения);
+ Наблюдатель = Новый НаблюдательЗаписывающий;
+ Источник.ДобавитьНаблюдателя(Наблюдатель);
+
+ МенеджерСущностей = Новый МенеджерСущностей(Источник);
+ МенеджерСущностей.ДобавитьКлассВМодель(Тип("ПростойОбъект"));
+ МенеджерСущностей.Инициализировать();
+
+ Если ТипКоннектора <> Тип("КоннекторInMemory") Тогда
+ МенеджерСущностей.НачатьТранзакцию();
+ КонецЕсли;
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+ Если ТипКоннектора <> Тип("КоннекторInMemory") Тогда
+ МенеджерСущностей.ЗафиксироватьТранзакцию();
+ КонецЕсли;
+ МенеджерСущностей.Закрыть();
+
+ Возврат Наблюдатель;
+
+КонецФункции
+
+Функция НовыйПростойОбъект(Знач Идентификатор)
+ Объект = Новый ПростойОбъект;
+ Объект.Идентификатор = Строка(Идентификатор);
+ Объект.Поле = "поле " + Идентификатор;
+ Возврат Объект;
+КонецФункции
+
+Процедура ПодключитьФикстуры()
+
+ Фикстуры = Новый Массив;
+ Фикстуры.Добавить("ПростойОбъект");
+ Фикстуры.Добавить("НаблюдательЗаписывающий");
+ Фикстуры.Добавить("КоннекторТранзакцийДляТестов");
+
+ Для Каждого Фикстура Из Фикстуры Цикл
+ ПодключитьСценарий(
+ ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", Фикстура + ".os"),
+ Фикстура
+ );
+ КонецЦикла;
+
+КонецПроцедуры
diff --git "a/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os" "b/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os"
index 2efc042c..3e20a0c7 100644
--- "a/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os"
+++ "b/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os"
@@ -62,11 +62,23 @@
Процедура КоннекторSQLiteРеализуетИнтерфейсКоннектора() Экспорт
ИнтерфейсОбъекта = Новый ИнтерфейсОбъекта();
ИнтерфейсОбъекта.ИзОбъекта(Тип("АбстрактныйКоннектор"));
-
+
РефлекторОбъекта = Новый РефлекторОбъекта(Тип("КоннекторSQLite"));
РефлекторОбъекта.РеализуетИнтерфейс(ИнтерфейсОбъекта, Истина);
КонецПроцедуры
+// Описание соединения - экспортный метод интерфейса НаблюдаемыйКоннектор, и позвать его могут
+// раньше открытия. Отдавать в полях структуры Неопределено вместо пустой строки в таком случае
+// незачем: поля описания - строковые.
+&Тест
+Процедура ОписаниеНеоткрытогоКоннектораБезНеопределено() Экспорт
+
+ Описание = Новый КоннекторSQLite().ОписаниеСоединения();
+
+ Ожидаем.Что(Описание.БазаДанных, "База данных неоткрытого коннектора - пустая строка").Равно("");
+
+КонецПроцедуры
+
&Тест
Процедура Сохранить() Экспорт
diff --git "a/tests/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\227\320\260\320\277\321\200\320\276\321\201\320\276\320\262.os" "b/tests/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\227\320\260\320\277\321\200\320\276\321\201\320\276\320\262.os"
new file mode 100644
index 00000000..1ccfe9d3
--- /dev/null
+++ "b/tests/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\227\320\260\320\277\321\200\320\276\321\201\320\276\320\262.os"
@@ -0,0 +1,283 @@
+// Наблюдатели источника данных: события уровня запроса у SQL-коннекторов и описание соединения.
+//
+// База выбирается как в остальных наборах: SQLite, если разрешен, иначе PostgreSQL.
+// У обоих коннекторов события запроса несут текст SQL с плейсхолдерами параметров.
+//
+// BSLLS:MagicNumber-off
+// BSLLS:DuplicateStringLiteral-off
+// BSLLS:UsingHardcodeNetworkAddress-off
+
+#Использовать ".."
+#Использовать "utils"
+
+// Источник данных, из которого тесты создают менеджер.
+Перем Источник;
+// Менеджер, который тесты создают заново перед каждым тестом.
+Перем МенеджерСущностей;
+// Наблюдатель, зарегистрированный до инициализации менеджера.
+Перем Наблюдатель;
+// Тип коннектора тестовой БД.
+Перем ТипКоннектораТестовойБазы;
+// Строка соединения с тестовой БД.
+Перем СтрокаСоединенияКоннектора;
+
+&ПередКаждым
+Процедура ПередЗапускомТеста() Экспорт
+
+ ЗапускатьТестыPostgres = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("TESTRUNNER_RUN_POSTGRES_TESTS", "true");
+ ЗапускатьТестыSQLite = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("TESTRUNNER_RUN_SQLITE_TESTS", "true");
+
+ ВыполнятьСбросТаблиц = Ложь;
+ Если ЗапускатьТестыSQLite = "true" Тогда
+ СтрокаСоединенияКоннектора = "FullUri=file::memory:?cache=shared";
+ ТипКоннектораТестовойБазы = Тип("КоннекторSQLite");
+ ИначеЕсли ЗапускатьТестыPostgres = "true" Тогда
+ СтрокаСоединенияКоннектора = СтрокаСоединенияPostgres();
+ ТипКоннектораТестовойБазы = Тип("КоннекторPostgreSQL");
+ ВыполнятьСбросТаблиц = Истина;
+ Иначе
+ ВызватьИсключение "Нет доступного коннектора для тестирования наблюдателей запросов";
+ КонецЕсли;
+
+ ПодключитьФикстуры();
+
+ Источник = Новый ИсточникДанных("запросы", ТипКоннектораТестовойБазы, СтрокаСоединенияКоннектора);
+ // Подготовка и тест идут в разных потоках: с автозакрытием источник умер бы до теста.
+ Источник.УстановитьАвтоЗакрытие(Ложь);
+ Наблюдатель = Новый НаблюдательЗаписывающий;
+ Источник.ДобавитьНаблюдателя(Наблюдатель);
+
+ МенеджерСущностей = Новый МенеджерСущностей(Источник);
+
+ Если ВыполнятьСбросТаблиц Тогда
+ ТестовыеУтилиты.СброситьТаблицы(МенеджерСущностей);
+ КонецЕсли;
+
+ МенеджерСущностей.ДобавитьКлассВМодель(Тип("СущностьБезГенерируемогоИдентификатора"));
+ МенеджерСущностей.ДобавитьКлассВМодель(Тип("Автор"));
+
+ МенеджерСущностей.Инициализировать();
+
+КонецПроцедуры
+
+&ПослеКаждого
+Процедура ПослеЗапускаТеста() Экспорт
+ МенеджерСущностей.ВыполнитьСКоннектором("Коннектор -> Коннектор.ВыполнитьЗапрос(""DELETE FROM Авторы"")");
+ МенеджерСущностей.Закрыть();
+ Источник.Закрыть();
+КонецПроцедуры
+
+&Тест
+Процедура ИнициализацияПорождаетСобытияСозданияТаблиц() Экспорт
+
+ Создания = Наблюдатель.Завершенные("Запрос", "CREATE TABLE");
+
+ Ожидаем.Что(Создания, "По событию на каждую таблицу модели").ИмеетДлину(2);
+ Ожидаем.Что(Создания[1].ИмяТаблицы()).Равно("Авторы");
+ Ожидаем.Что(Создания[1].ТекстЗапроса()).Содержит("CREATE TABLE IF NOT EXISTS Авторы");
+ Ожидаем.Что(Создания[1].Ошибка()).Равно(Неопределено);
+
+КонецПроцедуры
+
+&Тест
+Процедура СохранениеПорождаетСобытиеВставки() Экспорт
+
+ МенеджерСущностей.Сохранить(НовыйАвтор("Пушкин"));
+
+ Вставки = Наблюдатель.Завершенные("Запрос", "INSERT");
+
+ Ожидаем.Что(Вставки).ИмеетДлину(1);
+ Ожидаем.Что(Вставки[0].ИмяТаблицы()).Равно("Авторы");
+ Ожидаем.Что(Вставки[0].ТекстЗапроса()).Содержит("INSERT");
+ Ожидаем.Что(Вставки[0].ТекстЗапроса(), "Значения передаются параметрами, а не текстом").Не_().Содержит("Пушкин");
+ Ожидаем.Что(Вставки[0].Длительность()).Не_().Меньше(0);
+
+КонецПроцедуры
+
+&Тест
+Процедура ЧтениеПорождаетСобытиеВыборкиСЧисломСтрок() Экспорт
+
+ МенеджерСущностей.Сохранить(НовыйАвтор("Пушкин"));
+ МенеджерСущностей.Сохранить(НовыйАвтор("Гоголь"));
+
+ МенеджерСущностей.Получить(Тип("Автор"));
+
+ Выборки = Наблюдатель.Завершенные("Запрос", "SELECT");
+
+ Ожидаем.Что(Выборки).ИмеетДлину(1);
+ Ожидаем.Что(Выборки[0].ИмяТаблицы()).Равно("Авторы");
+ Ожидаем.Что(Выборки[0].ТекстЗапроса()).Содержит("SELECT * FROM Авторы");
+ Ожидаем.Что(Выборки[0].КоличествоСтрок()).Равно(2);
+
+ Операции = Наблюдатель.Завершенные("Операция", "Получить");
+ Ожидаем.Что(Выборки[0].Начало(), "Запрос начинается не раньше операции").Не_().Меньше(Операции[0].Начало());
+
+КонецПроцедуры
+
+&Тест
+Процедура УдалениеПорождаетСобытиеУдаления() Экспорт
+
+ Автор = НовыйАвтор("Пушкин");
+ МенеджерСущностей.Сохранить(Автор);
+ МенеджерСущностей.Удалить(Автор);
+
+ Удаления = Наблюдатель.Завершенные("Запрос", "DELETE");
+
+ Ожидаем.Что(Удаления).ИмеетДлину(1);
+ Ожидаем.Что(Удаления[0].ИмяТаблицы()).Равно("Авторы");
+ Ожидаем.Что(Удаления[0].ТекстЗапроса()).Содержит("DELETE FROM Авторы");
+
+КонецПроцедуры
+
+&Тест
+Процедура ТранзакцияПорождаетСобытияКомандТранзакции() Экспорт
+
+ МенеджерСущностей.НачатьТранзакцию();
+ МенеджерСущностей.Сохранить(НовыйАвтор("Пушкин"));
+ МенеджерСущностей.ЗафиксироватьТранзакцию();
+
+ МенеджерСущностей.НачатьТранзакцию();
+ МенеджерСущностей.Сохранить(НовыйАвтор("Гоголь"));
+ МенеджерСущностей.ОтменитьТранзакцию();
+
+ Ожидаем.Что(Наблюдатель.Завершенные("Запрос", "BEGIN")).ИмеетДлину(2);
+ Ожидаем.Что(Наблюдатель.Завершенные("Запрос", "COMMIT")).ИмеетДлину(1);
+ Ожидаем.Что(Наблюдатель.Завершенные("Запрос", "ROLLBACK")).ИмеетДлину(1);
+
+ Начало = Наблюдатель.Завершенные("Запрос", "BEGIN")[0];
+ Ожидаем.Что(Начало.ТекстЗапроса()).Содержит("TRANSACTION");
+ Ожидаем.Что(Начало.ИмяТаблицы()).Равно("");
+
+КонецПроцедуры
+
+&Тест
+Процедура ПроизвольныйЗапросПорождаетСобытиеСОперациейИзТекста() Экспорт
+
+ Строки = МенеджерСущностей.ВычислитьСКоннектором("Коннектор -> Коннектор.ВыполнитьЗапрос(""SELECT 1 AS Единица"")");
+
+ Ожидаем.Что(Строки).ИмеетДлину(1);
+
+ Выборки = Наблюдатель.Завершенные("Запрос", "SELECT");
+ Ожидаем.Что(Выборки).ИмеетДлину(1);
+ Ожидаем.Что(Выборки[0].ТекстЗапроса()).Равно("SELECT 1 AS Единица");
+ Ожидаем.Что(Выборки[0].ИмяТаблицы()).Равно("");
+ Ожидаем.Что(Выборки[0].КоличествоСтрок()).Равно(1);
+
+КонецПроцедуры
+
+&Тест
+Процедура ОшибкаЗапросаПопадаетВСобытиеИДоходитДоВызывающего() Экспорт
+
+ Параметры = Новый Массив;
+ Параметры.Добавить("Коннектор -> Коннектор.ВыполнитьЗапрос(""SELECT * FROM НетТакойТаблицы"")");
+
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("ВычислитьСКоннектором", Параметры)
+ .ВыбрасываетИсключение("НетТакойТаблицы");
+
+ Выборки = Наблюдатель.Завершенные("Запрос", "SELECT");
+ Ожидаем.Что(Выборки).ИмеетДлину(1);
+ Ожидаем.Что(Выборки[0].Ошибка()).ИмеетТип("ИнформацияОбОшибке");
+ Ожидаем.Что(Выборки[0].Длительность()).Не_().Меньше(0);
+
+ Операции = Наблюдатель.Завершенные("Операция", "ВычислитьСКоннектором");
+ Ожидаем.Что(Операции[0].Ошибка(), "Ошибка запроса поднимается и в операцию").ИмеетТип("ИнформацияОбОшибке");
+
+КонецПроцедуры
+
+&Тест
+Процедура СобытияНесутОписаниеСоединенияБезСекретов() Экспорт
+
+ МенеджерСущностей.Сохранить(НовыйАвтор("Пушкин"));
+
+ Событие = Наблюдатель.Завершенные("Запрос", "INSERT")[0];
+
+ Если ТипКоннектораТестовойБазы = Тип("КоннекторSQLite") Тогда
+ Ожидаем.Что(Событие.Система()).Равно("sqlite");
+ Ожидаем.Что(Событие.БазаДанных(), "URI без параметров запроса: в них бывают секреты").Равно("file::memory:");
+ Ожидаем.Что(Событие.Адрес()).Равно("");
+ Ожидаем.Что(Событие.Порт()).Равно(0);
+ Иначе
+ Ожидаем.Что(Событие.Система()).Равно("postgresql");
+ Ожидаем.Что(Событие.Адрес()).Равно(ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_HOST", "localhost"));
+ ОжидаемыйПорт = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_PORT", "5432");
+ ОжидаемаяБаза = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_DATABASE", "postgres");
+ Ожидаем.Что(Строка(Событие.Порт())).Равно(ОжидаемыйПорт);
+ Ожидаем.Что(Событие.БазаДанных()).Равно(ОжидаемаяБаза);
+ КонецЕсли;
+
+ Операция = Наблюдатель.Завершенные("Операция", "Сохранить")[0];
+ Ожидаем.Что(Операция.Система(), "Описание соединения есть и у событий операций").Равно(Событие.Система());
+
+КонецПроцедуры
+
+// Проверка идет по строке с заведомо уникальным паролем, а не по паролю тестовой базы:
+// в CI пароль, пользователь и база называются одинаково, и "не содержит пароль"
+// там неотличимо от "не содержит имя базы".
+&Тест
+Процедура ОписаниеСоединенияНеСодержитПароля() Экспорт
+
+ Пароль = "СекретныйПарольНаблюдателей";
+ СтрокаСоединения = "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=" + Пароль;
+
+ Описание = СтрокиСоединения.ОписаниеСервера("postgresql", СтрокаСоединения, 5432);
+
+ Ожидаем.Что(Описание.Количество(), "В описании только система, адрес, порт и база").Равно(4);
+ Для Каждого Поле Из Описание Цикл
+ Ожидаем.Что(Строка(Поле.Значение), Поле.Ключ).Не_().Содержит(Пароль);
+ КонецЦикла;
+ Ожидаем.Что(Описание.БазаДанных).Равно("postgres");
+
+КонецПроцедуры
+
+&Тест
+Процедура ПортИзСтрокиСоединенияПриводитсяКЧислуИлиБеретсяПоУмолчанию() Экспорт
+
+ Ожидаем.Что(СтрокиСоединения.ОписаниеСервера("postgresql", "Host=db;Port=6543", 5432).Порт).Равно(6543);
+ Ожидаем.Что(СтрокиСоединения.ОписаниеСервера("postgresql", "Host=db;Port=нет", 5432).Порт).Равно(5432);
+ Ожидаем.Что(СтрокиСоединения.ОписаниеСервера("postgresql", "Host=db", 5432).Порт).Равно(5432);
+
+КонецПроцедуры
+
+// Служебные
+
+Функция НовыйАвтор(Знач Имя)
+ Автор = Новый Автор;
+ Автор.Имя = Имя;
+ Возврат Автор;
+КонецФункции
+
+Процедура ПодключитьФикстуры()
+
+ Фикстуры = Новый Массив;
+ Фикстуры.Добавить("СущностьБезГенерируемогоИдентификатора");
+ Фикстуры.Добавить("Автор");
+ Фикстуры.Добавить("НаблюдательЗаписывающий");
+
+ Для Каждого Фикстура Из Фикстуры Цикл
+ ПодключитьСценарий(
+ ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", Фикстура + ".os"),
+ Фикстура
+ );
+ КонецЦикла;
+
+КонецПроцедуры
+
+Функция СтрокаСоединенияPostgres()
+
+ Хост = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_HOST", "localhost");
+ Порт = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_PORT", "5432");
+ Пользователь = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_USERNAME", "postgres");
+ Пароль = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_PASSWORD", "postgres");
+ ИмяБД = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_DATABASE", "postgres");
+
+ Возврат СтрШаблон(
+ "Host=%1;Port=%2;Username=%3;Password=%4;Database=%5;",
+ Хост,
+ Порт,
+ Пользователь,
+ Пароль,
+ ИмяБД
+ );
+
+КонецФункции
diff --git "a/tests/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os" "b/tests/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
new file mode 100644
index 00000000..51e35a69
--- /dev/null
+++ "b/tests/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\260\320\224\320\260\320\275\320\275\321\213\321\205.os"
@@ -0,0 +1,556 @@
+// Наблюдатели источника данных: регистрация и события уровня операции.
+//
+// Наблюдатель регистрируется на источнике данных, а менеджер создается из источника и отдает
+// наблюдателям события всех операций: самого менеджера, его хранилищ и Active Record.
+// База - InMemory: события операций от коннектора не зависят.
+//
+// BSLLS:MagicNumber-off
+// BSLLS:DuplicateStringLiteral-off
+
+#Использовать ".."
+#Использовать "utils"
+
+// Источник данных, на котором зарегистрирован наблюдатель.
+Перем Источник;
+// Менеджер, который тесты создают заново перед каждым тестом.
+Перем МенеджерСущностей;
+// Наблюдатель, зарегистрированный на источнике до создания менеджера.
+Перем Наблюдатель;
+// Массив из ИсточникДанных - Источники, созданные тестом: тест может заменить источник новым,
+// и закрыть нужно все, а не только последний.
+Перем СозданныеИсточники;
+
+&ПередКаждым
+Процедура ПередЗапускомТеста() Экспорт
+
+ ПодключитьФикстуры();
+
+ ХранилищеВПамяти.Очистить("наблюдатели");
+
+ СозданныеИсточники = Новый Массив;
+
+ СоздатьМенеджерСНаблюдателем();
+
+КонецПроцедуры
+
+// Создает источник с зарегистрированным наблюдателем и менеджер из него, не трогая данные базы в памяти.
+Процедура СоздатьМенеджерСНаблюдателем()
+
+ Источник = Новый ИсточникДанных("наблюдатели", Тип("КоннекторInMemory"), "наблюдатели");
+ СозданныеИсточники.Добавить(Источник);
+ // Подготовка и тест идут в разных потоках: с автозакрытием источник умер бы до теста.
+ Источник.УстановитьАвтоЗакрытие(Ложь);
+ Наблюдатель = Новый НаблюдательЗаписывающий;
+ Источник.ДобавитьНаблюдателя(Наблюдатель);
+
+ МенеджерСущностей = НовыйМенеджер(Источник);
+
+КонецПроцедуры
+
+// Создает и инициализирует менеджер из источника с моделью тестовых сущностей.
+//
+// Параметры:
+// ИсточникМенеджера - ИсточникДанных - Источник, из которого создается менеджер.
+//
+// Возвращаемое значение:
+// МенеджерСущностей - Инициализированный менеджер.
+//
+Функция НовыйМенеджер(Знач ИсточникМенеджера)
+
+ Менеджер = Новый МенеджерСущностей(ИсточникМенеджера);
+ Менеджер.ДобавитьКлассВМодель(Тип("ПростойОбъект"));
+ Менеджер.ДобавитьКлассВМодель(Тип("СущностьБезГенерируемогоИдентификатора"));
+ Менеджер.ДобавитьКлассВМодель(Тип("Автор"));
+ Менеджер.Инициализировать();
+
+ Возврат Менеджер;
+
+КонецФункции
+
+&ПослеКаждого
+Процедура ПослеЗапускаТеста() Экспорт
+ МенеджерСущностей.Закрыть();
+ ТестовыеУтилиты.ЗакрытьИсточники(СозданныеИсточники);
+КонецПроцедуры
+
+// Регистрация
+
+&Тест
+Процедура ДобавитьНаблюдателяОтвергаетОбъектБезОбъявленияИнтерфейса() Экспорт
+
+ Параметры = Новый Массив;
+ Параметры.Добавить(Новый ПростойОбъект);
+
+ Ожидаем.Что(Источник)
+ .Метод("ДобавитьНаблюдателя", Параметры)
+ .ВыбрасываетИсключение("НаблюдательИсточникаДанных");
+
+КонецПроцедуры
+
+&Тест
+Процедура ДобавитьНаблюдателяОтвергаетНеполнуюРеализацию() Экспорт
+
+ Параметры = Новый Массив;
+ Параметры.Добавить(Новый НаблюдательНеполный);
+
+ // Текст об интерфейсе дает валидатор extends: недостающий метод он не называет.
+ Ожидаем.Что(Источник)
+ .Метод("ДобавитьНаблюдателя", Параметры)
+ .ВыбрасываетИсключение("Не реализован требуемый интерфейс");
+
+КонецПроцедуры
+
+&Тест
+Процедура УдаленныйНаблюдательСобытийНеПолучает() Экспорт
+
+ Источник.УдалитьНаблюдателя(Наблюдатель);
+ КоличествоДоУдаления = Наблюдатель.Записи.Количество();
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ Ожидаем.Что(Наблюдатель.Записи).ИмеетДлину(КоличествоДоУдаления);
+
+КонецПроцедуры
+
+&Тест
+Процедура МенеджерБеретРазмерПулаИзИсточника() Экспорт
+
+ ИсточникСПулом = Новый ИсточникДанных("пул", Тип("КоннекторInMemory"), "наблюдатели", , 3);
+ СозданныеИсточники.Добавить(ИсточникСПулом);
+ НаблюдательПула = Новый НаблюдательЗаписывающий;
+ ИсточникСПулом.ДобавитьНаблюдателя(НаблюдательПула);
+
+ Менеджер = НовыйМенеджер(ИсточникСПулом);
+ Менеджер.Закрыть();
+
+ Захваты = НаблюдательПула.Завершенные("Соединение", "Занять");
+ Ожидаем.Что(Захваты, "Инициализация занимала соединение").Не_().ИмеетДлину(0);
+ Ожидаем.Что(Захваты[0].Предел()).Равно(3);
+
+КонецПроцедуры
+
+&Тест
+Процедура МенеджерыОдногоИсточникаДелятНаблюдателей() Экспорт
+
+ Второй = НовыйМенеджер(Источник);
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+ Второй.Сохранить(НовыйПростойОбъект(2));
+ Второй.Закрыть();
+
+ Ожидаем.Что(Наблюдатель.Завершенные("Операция", "Сохранить"), "События обоих менеджеров").ИмеетДлину(2);
+ Ожидаем.Что(Наблюдатель.Завершенные("Операция", "Инициализировать")).ИмеетДлину(2);
+
+КонецПроцедуры
+
+&Тест
+Процедура НаблюдательДобавленныйПослеСозданияМенеджераПолучаетСобытия() Экспорт
+
+ Поздний = Новый НаблюдательЗаписывающий;
+ Источник.ДобавитьНаблюдателя(Поздний);
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ Ожидаем.Что(Поздний.Завершенные("Операция", "Сохранить"), "Реестр у источника и менеджера общий").ИмеетДлину(1);
+
+КонецПроцедуры
+
+// Операции
+
+&Тест
+Процедура СохранениеПорождаетСобытиеОперации() Экспорт
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ Начатые = Наблюдатель.Начатые("Операция", "Сохранить");
+ Завершенные = Наблюдатель.Завершенные("Операция", "Сохранить");
+
+ Ожидаем.Что(Начатые).ИмеетДлину(1);
+ Ожидаем.Что(Завершенные).ИмеетДлину(1);
+ Ожидаем.Что(Завершенные[0], "В обеих фазах приходит один и тот же объект события").Равно(Начатые[0]);
+
+ Событие = Завершенные[0];
+ Ожидаем.Что(Событие).ИмеетТип("СобытиеИсточникаДанных");
+ Ожидаем.Что(Событие.Вид()).Равно("Операция");
+ Ожидаем.Что(Событие.Операция()).Равно("Сохранить");
+ Ожидаем.Что(Событие.ТипСущности()).Равно(Тип("ПростойОбъект"));
+ Ожидаем.Что(Событие.ИмяТаблицы()).Равно("ПростыеОбъекты");
+ Ожидаем.Что(Событие.ТипКоннектора()).Равно(Тип("КоннекторInMemory"));
+ Ожидаем.Что(Событие.Глубина()).Равно(0);
+ Ожидаем.Что(Событие.Длительность()).Не_().Меньше(0);
+ Ожидаем.Что(Событие.Ошибка()).Равно(Неопределено);
+
+КонецПроцедуры
+
+&Тест
+Процедура ЧтениеСообщаетКоличествоПрочитанныхСтрок() Экспорт
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(2));
+
+ МенеджерСущностей.Получить(Тип("ПростойОбъект"));
+ МенеджерСущностей.ПолучитьОдно(Тип("ПростойОбъект"), "2");
+
+ Чтения = Наблюдатель.Завершенные("Операция", "Получить");
+ Ожидаем.Что(Чтения).ИмеетДлину(1);
+ Ожидаем.Что(Чтения[0].КоличествоСтрок()).Равно(2);
+
+ ЧтенияОдного = Наблюдатель.Завершенные("Операция", "ПолучитьОдно");
+ Ожидаем.Что(ЧтенияОдного).ИмеетДлину(1);
+ Ожидаем.Что(ЧтенияОдного[0].КоличествоСтрок()).Равно(1);
+
+КонецПроцедуры
+
+&Тест
+Процедура УдалениеПорождаетСобытиеОперации() Экспорт
+
+ Объект = НовыйПростойОбъект(1);
+ МенеджерСущностей.Сохранить(Объект);
+ МенеджерСущностей.Удалить(Объект);
+
+ Удаления = Наблюдатель.Завершенные("Операция", "Удалить");
+ Ожидаем.Что(Удаления).ИмеетДлину(1);
+ Ожидаем.Что(Удаления[0].ТипСущности()).Равно(Тип("ПростойОбъект"));
+
+КонецПроцедуры
+
+&Тест
+Процедура ИнициализацияПорождаетСобытиеОперации() Экспорт
+
+ Инициализации = Наблюдатель.Завершенные("Операция", "Инициализировать");
+
+ Ожидаем.Что(Инициализации).ИмеетДлину(1);
+ Ожидаем.Что(Инициализации[0].ТипСущности()).Равно(Неопределено);
+ Ожидаем.Что(Инициализации[0].Ошибка()).Равно(Неопределено);
+
+КонецПроцедуры
+
+&Тест
+Процедура ПрямойДоступПорождаетСобытиеОперации() Экспорт
+
+ Результат = МенеджерСущностей.ВычислитьСКоннектором("Коннектор -> ТипЗнч(Коннектор)");
+ МенеджерСущностей.ВыполнитьСКоннектором("Коннектор -> Коннектор.Открыт()");
+
+ Ожидаем.Что(Результат).Равно(Тип("КоннекторInMemory"));
+ Ожидаем.Что(Наблюдатель.Завершенные("Операция", "ВычислитьСКоннектором")).ИмеетДлину(1);
+ Ожидаем.Что(Наблюдатель.Завершенные("Операция", "ВыполнитьСКоннектором")).ИмеетДлину(1);
+
+КонецПроцедуры
+
+&Тест
+Процедура РазыменованиеСсылкиДаетВложенноеСобытие() Экспорт
+
+ Внешняя = Новый СущностьБезГенерируемогоИдентификатора;
+ Внешняя.ВнутреннийИдентификатор = 7;
+ МенеджерСущностей.Сохранить(Внешняя);
+
+ Автор = Новый Автор;
+ Автор.Имя = "Пушкин";
+ Автор.ВнешняяСущность = Внешняя;
+ МенеджерСущностей.Сохранить(Автор);
+
+ // Пул сущностей уже знает внешнюю сущность, поэтому читаем новым менеджером над теми же данными.
+ МенеджерСущностей.Закрыть();
+ СоздатьМенеджерСНаблюдателем();
+
+ Авторы = МенеджерСущностей.Получить(Тип("Автор"));
+ Ожидаем.Что(Авторы).ИмеетДлину(1);
+
+ Внешнее = Наблюдатель.Завершенные("Операция", "Получить");
+ Вложенные = Наблюдатель.Завершенные("Операция", "ПолучитьОдно");
+
+ Ожидаем.Что(Внешнее).ИмеетДлину(1);
+ Ожидаем.Что(Внешнее[0].Глубина()).Равно(0);
+ Ожидаем.Что(Внешнее[0].ТипСущности()).Равно(Тип("Автор"));
+
+ Ожидаем.Что(Вложенные).ИмеетДлину(1);
+ Ожидаем.Что(Вложенные[0].Глубина()).Равно(1);
+ Ожидаем.Что(Вложенные[0].ТипСущности()).Равно(Тип("СущностьБезГенерируемогоИдентификатора"));
+
+КонецПроцедуры
+
+&Тест
+Процедура ОшибкаОперацииПопадаетВСобытиеИДоходитДоВызывающего() Экспорт
+
+ Параметры = Новый Массив;
+ Параметры.Добавить("Коннектор -> Коннектор.НесуществующийМетод()");
+
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("ВыполнитьСКоннектором", Параметры)
+ .ВыбрасываетИсключение("НесуществующийМетод");
+
+ Ошибочные = Наблюдатель.Завершенные("Операция", "ВыполнитьСКоннектором");
+ Ожидаем.Что(Ошибочные).ИмеетДлину(1);
+ Ожидаем.Что(Ошибочные[0].Ошибка()).ИмеетТип("ИнформацияОбОшибке");
+ Ожидаем.Что(Ошибочные[0].Ошибка().Описание).Содержит("НесуществующийМетод");
+
+КонецПроцедуры
+
+&Тест
+Процедура ИсключениеНаблюдателяНеЛомаетОперацию() Экспорт
+
+ СломанныйНаблюдатель = Новый НаблюдательЗаписывающий(Истина);
+ Источник.ДобавитьНаблюдателя(СломанныйНаблюдатель);
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ Ожидаем.Что(МенеджерСущностей.Получить(Тип("ПростойОбъект"))).ИмеетДлину(1);
+ Ожидаем.Что(Наблюдатель.Завершенные("Операция", "Сохранить"), "Исправный наблюдатель события получает").ИмеетДлину(1);
+ ЗавершенияСломанного = СломанныйНаблюдатель.Завершенные("Операция", "Сохранить");
+ Ожидаем.Что(ЗавершенияСломанного, "Сломанному наблюдателю завершение все равно приходит").ИмеетДлину(1);
+
+КонецПроцедуры
+
+&Тест
+Процедура СобытияХранилищаИАктивнойЗаписиПриходятНаблюдателямИсточникаДанных() Экспорт
+
+ Хранилище = МенеджерСущностей.ПолучитьХранилищеСущностей(Тип("ПростойОбъект"));
+ Хранилище.Сохранить(НовыйПростойОбъект(1));
+
+ Элемент = МенеджерСущностей.СоздатьЭлемент(Тип("ПростойОбъект"));
+ Элемент.Идентификатор = 2;
+ Элемент.Сохранить();
+
+ Ожидаем.Что(Наблюдатель.Завершенные("Операция", "Сохранить")).ИмеетДлину(2);
+
+КонецПроцедуры
+
+&Тест
+Процедура СостояниеНаблюдателяЖиветВСобытииМеждуФазами() Экспорт
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ Событие = Наблюдатель.Завершенные("Операция", "Сохранить")[0];
+ Ожидаем.Что(Событие.Состояние()).Равно(Неопределено);
+
+ Событие.УстановитьСостояние("метка");
+ Ожидаем.Что(Событие.Состояние()).Равно("метка");
+
+КонецПроцедуры
+
+&Тест
+Процедура СостояниеУКаждогоНаблюдателяСвое() Экспорт
+
+ // Дано - два наблюдателя кладут в событие разные метки
+ Первый = Новый НаблюдательСМеткой("первый");
+ Второй = Новый НаблюдательСМеткой("второй");
+ Источник.ДобавитьНаблюдателя(Первый);
+ Источник.ДобавитьНаблюдателя(Второй);
+
+ // Когда
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ // Тогда - каждый в ПослеСобытия читает свою метку, а не последнюю записанную
+ Ожидаем.Что(Первый.Увиденные).Не_().ИмеетДлину(0);
+ Ожидаем.Что(Второй.Увиденные).ИмеетДлину(Первый.Увиденные.Количество());
+ Для Каждого Состояние Из Первый.Увиденные Цикл
+ Ожидаем.Что(Состояние).Равно("первый");
+ КонецЦикла;
+ Для Каждого Состояние Из Второй.Увиденные Цикл
+ Ожидаем.Что(Состояние).Равно("второй");
+ КонецЦикла;
+
+ // Записывающий наблюдатель ничего не клал - и ничего не видит
+ Событие = Наблюдатель.Завершенные("Операция", "Сохранить")[0];
+ Ожидаем.Что(Событие.Состояние()).Равно(Неопределено);
+
+КонецПроцедуры
+
+&Тест
+Процедура СнятыйВоВремяСобытияНаблюдательПолучаетЕгоЗавершение() Экспорт
+
+ // Дано - наблюдатель, который в первом же ПередСобытием снимает себя с регистрации
+ Отписывающийся = Новый НаблюдательПереподписывающийся(Источник);
+ Источник.ДобавитьНаблюдателя(Отписывающийся);
+
+ // Когда
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ // Тогда - состав получателей фиксируется при начале события
+ Ожидаем.Что(Отписывающийся.Начатые, "Снял себя в первом событии, вложенных не видел").ИмеетДлину(1);
+ Ожидаем.Что(Отписывающийся.Завершенные, "Начатое событие доходит до завершения").ИмеетДлину(1);
+ Ожидаем.Что(Отписывающийся.Завершенные[0]).Равно(Отписывающийся.Начатые[0]);
+
+КонецПроцедуры
+
+&Тест
+Процедура ДобавленныйВоВремяСобытияНаблюдательЕгоЗавершенияНеПолучает() Экспорт
+
+ // Дано - наблюдатель, который в первом же ПередСобытием регистрирует другого
+ Новичок = Новый НаблюдательЗаписывающий;
+ Подписывающий = Новый НаблюдательПереподписывающийся(Источник, Новичок);
+ Источник.ДобавитьНаблюдателя(Подписывающий);
+
+ // Когда
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ // Тогда
+ Ожидаем.Что(Новичок.Завершенные("Операция", "Сохранить"), "Событие началось до регистрации").ИмеетДлину(0);
+ Ожидаем.Что(Новичок.Завершенные("Запрос", "INSERT"), "Вложенные события начались после").ИмеетДлину(1);
+
+КонецПроцедуры
+
+// Коннектор
+
+&Тест
+Процедура ОшибкаПодключенияНаблюдателейЗакрываетОткрытыйКоннектор() Экспорт
+
+ МенеджерСущностей.Закрыть();
+
+ // Наблюдатели на источнике не нужны: коннектор подключается к реестру всегда.
+ Источник = Новый ИсточникДанных("наблюдатели", Тип("КоннекторСЛоманымОписанием"), "наблюдатели");
+ СозданныеИсточники.Добавить(Источник);
+ МенеджерСущностей = Новый МенеджерСущностей(Источник);
+ МенеджерСущностей.ДобавитьКлассВМодель(Тип("ПростойОбъект"));
+
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("Инициализировать", Новый Массив)
+ .ВыбрасываетИсключение("Описание соединения недоступно");
+
+ Закрыт = ТекущийПоток().Данные.Получить(Новый КоннекторСЛоманымОписанием().КлючЗакрыт());
+ Ожидаем.Что(Закрыт, "Открытый коннектор закрыт после ошибки подключения наблюдателей").Равно(Истина);
+
+КонецПроцедуры
+
+&Тест
+Процедура НеполноНаблюдаемыйКоннекторОтвергаетсяКаждыйРаз() Экспорт
+
+ МенеджерСущностей.Закрыть();
+
+ Источник = Новый ИсточникДанных("неполный", Тип("КоннекторНеполноНаблюдаемый"), "неполный");
+ СозданныеИсточники.Добавить(Источник);
+ МенеджерСущностей = Новый МенеджерСущностей(Источник);
+ МенеджерСущностей.ДобавитьКлассВМодель(Тип("ПростойОбъект"));
+
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("Инициализировать", Новый Массив)
+ .ВыбрасываетИсключение("Не реализован требуемый интерфейс");
+
+ // Проверку нельзя считать пройденной раньше, чем она прошла: иначе второе открытие соединения
+ // пропустит валидатор и вместо разговора про интерфейс отдаст ошибку ненайденного метода.
+ Ожидаем.Что(МенеджерСущностей, "Второе открытие отвергается тем же валидатором")
+ .Метод("Инициализировать", Новый Массив)
+ .ВыбрасываетИсключение("Не реализован требуемый интерфейс");
+
+КонецПроцедуры
+
+&Тест
+Процедура ОперацииКоннектораВПамятиПорождаютСобытияЗапросаБезТекста() Экспорт
+
+ Объект = НовыйПростойОбъект(1);
+ МенеджерСущностей.Сохранить(Объект);
+ МенеджерСущностей.Получить(Тип("ПростойОбъект"));
+ МенеджерСущностей.Удалить(Объект);
+
+ Создания = Наблюдатель.Завершенные("Запрос", "CREATE TABLE");
+ Ожидаем.Что(Создания, "По событию на каждую таблицу модели").ИмеетДлину(3);
+
+ Вставки = Наблюдатель.Завершенные("Запрос", "INSERT");
+ Ожидаем.Что(Вставки).ИмеетДлину(1);
+ Ожидаем.Что(Вставки[0].ИмяТаблицы()).Равно("ПростыеОбъекты");
+ Ожидаем.Что(Вставки[0].ТекстЗапроса(), "Коннектор без SQL текста запроса не формирует").Равно(Неопределено);
+
+ Выборки = Наблюдатель.Завершенные("Запрос", "SELECT");
+ Ожидаем.Что(Выборки).ИмеетДлину(1);
+ Ожидаем.Что(Выборки[0].КоличествоСтрок()).Равно(1);
+
+ Ожидаем.Что(Наблюдатель.Завершенные("Запрос", "DELETE")).ИмеетДлину(1);
+
+КонецПроцедуры
+
+&Тест
+Процедура СобытияНесутОписаниеСоединения() Экспорт
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ Событие = Наблюдатель.Завершенные("Операция", "Сохранить")[0];
+ Ожидаем.Что(Событие.Система()).Равно("inmemory");
+ Ожидаем.Что(Событие.БазаДанных()).Равно("наблюдатели");
+ Ожидаем.Что(Событие.Адрес()).Равно("");
+ Ожидаем.Что(Событие.Порт()).Равно(0);
+
+ Запрос = Наблюдатель.Завершенные("Запрос", "INSERT")[0];
+ Ожидаем.Что(Запрос.Система()).Равно("inmemory");
+ Ожидаем.Что(Запрос.БазаДанных()).Равно("наблюдатели");
+
+КонецПроцедуры
+
+&Тест
+Процедура СобытияДоОткрытияПервогоСоединенияПолучаютОписаниеКЗавершению() Экспорт
+
+ // Инициализация и ее захват соединения начинаются, когда ни один коннектор еще не открыт
+ // и описания соединения нет; к завершению события оно уже известно.
+ Инициализация = Наблюдатель.Завершенные("Операция", "Инициализировать")[0];
+ ПервыйЗахват = Наблюдатель.Завершенные("Соединение", "Занять")[0];
+
+ Ожидаем.Что(Инициализация.БазаДанных()).Равно("наблюдатели");
+ Ожидаем.Что(ПервыйЗахват.Система()).Равно("inmemory");
+ Ожидаем.Что(ПервыйЗахват.БазаДанных()).Равно("наблюдатели");
+
+КонецПроцедуры
+
+&Тест
+Процедура КоннекторБезИнтерфейсаНаблюдаемыйКоннекторДаетТолькоСобытияОпераций() Экспорт
+
+ МенеджерСущностей.Закрыть();
+
+ Источник = Новый ИсточникДанных("наблюдатели", Тип("КоннекторБезНаблюдения"), "наблюдатели");
+ СозданныеИсточники.Добавить(Источник);
+ Наблюдатель = Новый НаблюдательЗаписывающий;
+ Источник.ДобавитьНаблюдателя(Наблюдатель);
+ МенеджерСущностей = Новый МенеджерСущностей(Источник);
+ МенеджерСущностей.ДобавитьКлассВМодель(Тип("ПростойОбъект"));
+ МенеджерСущностей.Инициализировать();
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+ Ожидаем.Что(МенеджерСущностей.Получить(Тип("ПростойОбъект"))).ИмеетДлину(1);
+
+ Ожидаем.Что(Наблюдатель.Завершенные("Операция")).ИмеетДлину(3);
+ Ожидаем.Что(Наблюдатель.Завершенные("Запрос")).ИмеетДлину(0);
+
+ Событие = Наблюдатель.Завершенные("Операция", "Сохранить")[0];
+ Ожидаем.Что(Событие.Система(), "Без описания от коннектора система выводится из имени типа")
+ .Равно(НРег("КоннекторБезНаблюдения"));
+ Ожидаем.Что(Событие.БазаДанных()).Равно("");
+
+КонецПроцедуры
+
+// Служебные
+
+// Колонка идентификатора ПростойОбъект строковая: без &Колонка тип колонки - Строка.
+//
+// Параметры:
+// Идентификатор - Строка, Число - Идентификатор объекта, приводится к строке.
+//
+// Возвращаемое значение:
+// ПростойОбъект - Объект с заполненными идентификатором и полем.
+//
+Функция НовыйПростойОбъект(Знач Идентификатор)
+ Объект = Новый ПростойОбъект;
+ Объект.Идентификатор = Строка(Идентификатор);
+ Объект.Поле = "поле " + Идентификатор;
+ Возврат Объект;
+КонецФункции
+
+Процедура ПодключитьФикстуры()
+
+ Фикстуры = Новый Массив;
+ Фикстуры.Добавить("ПростойОбъект");
+ Фикстуры.Добавить("СущностьБезГенерируемогоИдентификатора");
+ Фикстуры.Добавить("Автор");
+ Фикстуры.Добавить("НаблюдательЗаписывающий");
+ Фикстуры.Добавить("НаблюдательНеполный");
+ Фикстуры.Добавить("НаблюдательСМеткой");
+ Фикстуры.Добавить("НаблюдательПереподписывающийся");
+ Фикстуры.Добавить("КоннекторБезНаблюдения");
+ Фикстуры.Добавить("КоннекторСЛоманымОписанием");
+ Фикстуры.Добавить("КоннекторНеполноНаблюдаемый");
+
+ Для Каждого Фикстура Из Фикстуры Цикл
+ ПодключитьСценарий(
+ ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", Фикстура + ".os"),
+ Фикстура
+ );
+ КонецЦикла;
+
+КонецПроцедуры
diff --git "a/tests/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271.os" "b/tests/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271.os"
new file mode 100644
index 00000000..2ae84909
--- /dev/null
+++ "b/tests/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\320\270\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271.os"
@@ -0,0 +1,688 @@
+// Наблюдатели источника данных: события соединений пула и транзакций.
+//
+// База - InMemory за коннектором-обёрткой, изображающим транзакции: события соединений и
+// транзакций рождаются в пуле и в обёртке соединения, от СУБД они не зависят.
+//
+// BSLLS:MagicNumber-off
+// BSLLS:DuplicateStringLiteral-off
+
+#Использовать ".."
+#Использовать "utils"
+
+// Источник данных, из которого тесты создают менеджер.
+Перем Источник;
+// Менеджер, который тесты создают заново перед каждым тестом.
+Перем МенеджерСущностей;
+// Наблюдатель, зарегистрированный на менеджере до инициализации.
+Перем Наблюдатель;
+// ИнформацияОбОшибке - Ошибка фиксации транзакции в фоновом задании; Неопределено, если ее не было.
+Перем ОшибкаФиксацииВФоне;
+// Массив из ИсточникДанных - Источники, созданные тестом: тест может создать менеджер заново,
+// и закрыть нужно все источники, а не только последний.
+Перем СозданныеИсточники;
+
+&ПередКаждым
+Процедура ПередЗапускомТеста() Экспорт
+
+ ПодключитьФикстуры();
+ ХранилищеВПамяти.Очистить("соединения");
+
+ ОшибкаФиксацииВФоне = Неопределено;
+ СозданныеИсточники = Новый Массив;
+
+ СоздатьМенеджер(10);
+
+КонецПроцедуры
+
+&ПослеКаждого
+Процедура ПослеЗапускаТеста() Экспорт
+ МенеджерСущностей.Закрыть();
+ ТестовыеУтилиты.ЗакрытьИсточники(СозданныеИсточники);
+КонецПроцедуры
+
+// Соединения
+
+&Тест
+Процедура ЗахватИОсвобождениеСоединенияПорождаютСобытия() Экспорт
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ Захваты = Наблюдатель.Завершенные("Соединение", "Занять");
+ Освобождения = Наблюдатель.Завершенные("Соединение", "Освободить");
+
+ Ожидаем.Что(Захваты, "Инициализация и сохранение занимают соединение по разу").ИмеетДлину(2);
+ Ожидаем.Что(Освобождения).ИмеетДлину(2);
+
+ Первый = Захваты[0];
+ Ожидаем.Что(Первый.Открыто(), "Первый захват открывает новое соединение").Равно(Истина);
+ Ожидаем.Что(Первый.ВремяОткрытия()).Не_().Меньше(0);
+ Ожидаем.Что(Первый.Ожидание()).Равно(0);
+ Ожидаем.Что(Первый.Ошибка()).Равно(Неопределено);
+
+ Второй = Захваты[1];
+ Ожидаем.Что(Второй.Открыто(), "Второй захват берет свободное соединение").Равно(Ложь);
+ Ожидаем.Что(Второй.ВремяОткрытия()).Равно(0);
+ Ожидаем.Что(Второй.Занято(), "Снимок пула после захвата").Равно(1);
+ Ожидаем.Что(Второй.Свободно()).Равно(0);
+
+ Последнее = Освобождения[1];
+ Ожидаем.Что(Последнее.Занято(), "Снимок пула после освобождения").Равно(0);
+ Ожидаем.Что(Последнее.Свободно()).Равно(1);
+ Ожидаем.Что(Последнее.Ожидает()).Равно(0);
+ Ожидаем.Что(Последнее.Предел()).Равно(10);
+
+КонецПроцедуры
+
+&Тест
+Процедура ПовторныйЗахватТогоЖеПотокаСобытийНеПорождает() Экспорт
+
+ Внешняя = Новый СущностьБезГенерируемогоИдентификатора;
+ Внешняя.ВнутреннийИдентификатор = 7;
+ МенеджерСущностей.Сохранить(Внешняя);
+
+ Автор = Новый Автор;
+ Автор.Имя = "Пушкин";
+ Автор.ВнешняяСущность = Внешняя;
+ МенеджерСущностей.Сохранить(Автор);
+
+ МенеджерСущностей.Закрыть();
+ СоздатьМенеджер(10);
+
+ ЗахватовДо = Наблюдатель.Завершенные("Соединение", "Занять").Количество();
+ МенеджерСущностей.Получить(Тип("Автор"));
+
+ Ожидаем.Что(Наблюдатель.Завершенные("Операция", "ПолучитьОдно"), "Ссылка разыменована вложенной операцией")
+ .ИмеетДлину(1);
+ ЗахватовВложеннойОперацией = Наблюдатель.Завершенные("Соединение", "Занять").Количество() - ЗахватовДо;
+ Ожидаем.Что(ЗахватовВложеннойОперацией, "Вложенная операция идет по тому же соединению").Равно(1);
+
+КонецПроцедуры
+
+&Тест
+Процедура ОжиданиеСоединенияПопадаетВСобытие() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ СоздатьМенеджер(1);
+
+ Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ДержатьСоединениеВФоне", Новый Массив, Истина);
+ // Транзакция задания начинается на занятом им соединении: ее начало и есть сигнал, что
+ // единственное соединение пула занято и основному потоку придется ждать.
+ Ожидаем.Что(ДождатьсяНачатыхТранзакций(1), "Задание заняло соединение").ИмеетДлину(1);
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+ ДождатьсяЗадания(Задание);
+
+ Захваты = Наблюдатель.Завершенные("Соединение", "Занять");
+ Последний = Захваты[Захваты.ВГраница()];
+
+ Ожидаем.Что(Последний.Ожидание(), "Основной поток ждал соединение, занятое заданием").Не_().Меньше(100);
+ Ожидаем.Что(Последний.Открыто()).Равно(Ложь);
+
+ // Снимок берется после того, как ожидающий получил соединение: в очереди больше никого, и
+ // собственная отработавшая заявка потока в ожидающих числиться не должна.
+ Ожидаем.Что(Последний.Ожидает(), "Дождавшийся не считается ожидающим").Равно(0);
+ Ожидаем.Что(Источник.ПулСоединений().КоличествоОжидающих(), "Очередь пуста").Равно(0);
+
+КонецПроцедуры
+
+&Тест
+Процедура СобытиеЗахватаОхватываетОжиданиеБлокировкиПула() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ Настройки = Новый НастройкиКоннектораТранзакций;
+ Настройки.ПадатьПриЗавершенииТранзакции = Истина;
+ Настройки.ПаузаЗакрытия = 600;
+ Настройки.СигналЗакрытия = Новый СигналСобытия;
+ СоздатьМенеджер(1, Настройки);
+
+ // Задание портит единственное соединение пула неудачной фиксацией. Возврат испорченного
+ // соединения закрывает его, а закрытие идет под блокировкой записи пула - основному потоку
+ // придется стоять на ней, еще не начав захват.
+ Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ИспортитьСоединениеВФоне", Новый Массив, Истина);
+
+ // Ждем не события транзакции, а входа в закрытие: только он означает, что блокировка пула
+ // уже занята заданием. Пойди основной поток раньше - он занял бы соединение сам или встал
+ // в очередь, и мерить было бы нечего.
+ Ожидаем.Что(ДождатьсяСигнала(Настройки.СигналЗакрытия), "Задание дошло до закрытия соединения")
+ .ЭтоИстина();
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+ ДождатьсяЗадания(Задание);
+
+ Ожидаем.Что(ОшибкаФиксацииВФоне, "Задание должно было увидеть ошибку фиксации")
+ .ИмеетТип("ИнформацияОбОшибке");
+
+ Захваты = Наблюдатель.Завершенные("Соединение", "Занять");
+ Последний = Захваты[Захваты.ВГраница()];
+
+ Ожидаем.Что(Последний.Ожидание(), "В очередь поток не вставал: он стоял на блокировке").Равно(0);
+ Ожидаем.Что(Последний.Длительность(), "Событие захвата начинается до блокировки пула").Не_().Меньше(300);
+
+КонецПроцедуры
+
+&Тест
+Процедура СобытиеОсвобожденияОхватываетВозвратСоединения() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ Настройки = Новый НастройкиКоннектораТранзакций;
+ Настройки.ПадатьПриЗавершенииТранзакции = Истина;
+ Настройки.ПаузаЗакрытия = 600;
+ СоздатьМенеджер(10, Настройки);
+
+ МенеджерСущностей.НачатьТранзакцию();
+
+ // Неудачная фиксация делает соединение непригодным. Возврат такого соединения закрывает его,
+ // и закрытие идет под блокировкой записи пула, то есть внутри освобождения.
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("ЗафиксироватьТранзакцию", Новый Массив)
+ .ВыбрасываетИсключение("Завершение транзакции не удалось");
+
+ Освобождения = Наблюдатель.Завершенные("Соединение", "Освободить");
+ Последнее = Освобождения[Освобождения.ВГраница()];
+
+ Ожидаем.Что(Последнее.Длительность(), "Событие освобождения охватывает закрытие соединения")
+ .Не_().Меньше(300);
+
+КонецПроцедуры
+
+&Тест
+Процедура ОшибкаОсвобожденияНеОставляетСобытиеОперацииНезавершенным() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ Настройки = Новый НастройкиКоннектораТранзакций;
+ Настройки.ПадатьПриЗавершенииТранзакции = Истина;
+ // Падает только первое закрытие - то, которым пул избавляется от испорченного соединения.
+ Настройки.ПланЗакрытий = Новый ПланЗакрытийКоннектора(1, 1);
+ СоздатьМенеджер(10, Настройки);
+
+ Контекст = Новый Структура("Менеджер", МенеджерСущностей);
+ Параметры = Новый Массив;
+ Параметры.Добавить("Коннектор -> Менеджер.ЗафиксироватьТранзакцию()");
+ Параметры.Добавить(Контекст);
+
+ // Неудачная фиксация делает соединение непригодным, возврат такого соединения закрывает его,
+ // а закрытие падает: освобождение внутри операции завершается ошибкой. Наружу должна идти
+ // причина - ошибка операции, а не ошибка освобождения.
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("ВыполнитьСКоннектором", Параметры)
+ .ВыбрасываетИсключение("Завершение транзакции не удалось");
+
+ Операции = Наблюдатель.Завершенные("Операция", "ВыполнитьСКоннектором");
+ Ожидаем.Что(Операции, "Событие операции завершено, несмотря на ошибку освобождения").ИмеетДлину(1);
+ Ожидаем.Что(Операции[0].Ошибка()).ИмеетТип("ИнформацияОбОшибке");
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+
+ Сохранения = Наблюдатель.Завершенные("Операция", "Сохранить");
+ Ожидаем.Что(Сохранения[Сохранения.ВГраница()].Глубина(), "Вложенность операций потока не съехала").Равно(0);
+
+КонецПроцедуры
+
+&Тест
+Процедура ВозвратСоединенияУмершегоПотокаДаетСобытиеОсвобождения() Экспорт
+
+ ОсвобожденийДо = Наблюдатель.Завершенные("Соединение", "Освободить").Количество();
+
+ // Задание занимает соединение транзакцией и обрывается, не освободив его: соединение
+ // возвращает обработчик завершения потока, и это такой же возврат в пул.
+ Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ОборватьТранзакциюВФоне", Новый Массив, Истина);
+ ДождатьсяЗадания(Задание);
+
+ Освобождения = ДождатьсяОсвобождений(ОсвобожденийДо + 1);
+
+ Ожидаем.Что(Освобождения, "Возврат соединения умершего потока - тоже освобождение")
+ .ИмеетДлину(ОсвобожденийДо + 1);
+
+ Последнее = Освобождения[Освобождения.ВГраница()];
+ Ожидаем.Что(Последнее.Занято(), "Снимок пула после возврата").Равно(0);
+ Ожидаем.Что(Последнее.Свободно()).Равно(1);
+
+КонецПроцедуры
+
+&Тест
+Процедура ОшибкаЗакрытияНеОставляетЗахватБезОсвобождения() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ Настройки = Новый НастройкиКоннектораТранзакций;
+ Настройки.ПадатьПриЗавершенииТранзакции = Истина;
+ // Падает только первое закрытие - то, которым пул избавляется от испорченного соединения.
+ Настройки.ПланЗакрытий = Новый ПланЗакрытийКоннектора(1, 1);
+ СоздатьМенеджер(10, Настройки);
+
+ Контекст = Новый Структура("Менеджер", МенеджерСущностей);
+ Параметры = Новый Массив;
+ Параметры.Добавить("Коннектор -> Менеджер.ЗафиксироватьТранзакцию()");
+ Параметры.Добавить(Контекст);
+
+ // Соединение отобрано у потока и место в пуле возвращено еще до закрытия, поэтому возврат
+ // состоялся, как бы ни кончилось закрытие. Событие захвата без пары оставило бы наблюдателю
+ // незакрытый спан и незанятое, но так и не освобожденное соединение в счетчиках.
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("ВыполнитьСКоннектором", Параметры)
+ .ВыбрасываетИсключение("Завершение транзакции не удалось");
+
+ Захваты = Наблюдатель.Завершенные("Соединение", "Занять");
+ Освобождения = Наблюдатель.Завершенные("Соединение", "Освободить");
+
+ Ожидаем.Что(Освобождения, "У каждого захвата есть освобождение, даже если закрытие упало")
+ .ИмеетДлину(Захваты.Количество());
+
+ Последнее = Освобождения[Освобождения.ВГраница()];
+ Ожидаем.Что(Последнее.Ошибка(), "Ошибка закрытия - это ошибка освобождения")
+ .ИмеетТип("ИнформацияОбОшибке");
+
+КонецПроцедуры
+
+&Тест
+Процедура ОшибкаЗакрытияНеОтменяетОсвобождениеУмершегоПотока() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ Настройки = Новый НастройкиКоннектораТранзакций;
+ // Откат брошенной транзакции падает, соединение становится непригодным, и пул его закрывает -
+ // а закрытие роняет план.
+ Настройки.ПадатьПриЗавершенииТранзакции = Истина;
+ Настройки.ПланЗакрытий = Новый ПланЗакрытийКоннектора(1, 1);
+ СоздатьМенеджер(10, Настройки);
+
+ ОсвобожденийДо = Наблюдатель.Завершенные("Соединение", "Освободить").Количество();
+
+ Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ОборватьТранзакциюВФоне", Новый Массив, Истина);
+ ДождатьсяЗадания(Задание);
+
+ Освобождения = ДождатьсяОсвобождений(ОсвобожденийДо + 1);
+
+ Ожидаем.Что(Освобождения, "Возврат за умершим потоком - освобождение, даже если закрытие упало")
+ .ИмеетДлину(ОсвобожденийДо + 1);
+
+ Последнее = Освобождения[Освобождения.ВГраница()];
+ Ожидаем.Что(Последнее.Ошибка(), "Ошибка закрытия - это ошибка освобождения")
+ .ИмеетТип("ИнформацияОбОшибке");
+
+КонецПроцедуры
+
+&Тест
+Процедура НаблюдателиНеЗовутсяИзПодБлокировкиПула() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ СоздатьМенеджер(2);
+
+ // Наблюдатель засыпает на завершении события транзакции. Такое событие завершается при
+ // возврате брошенного соединения, то есть в тот момент, когда пул держит свою блокировку.
+ СигналПаузы = Новый СигналСобытия;
+ Тормоз = Новый НаблюдательТормозящий(ВидыСобытийИсточникаДанных.Транзакция(), 600, СигналПаузы);
+ Источник.ДобавитьНаблюдателя(Тормоз);
+
+ Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ОборватьТранзакциюВФоне", Новый Массив, Истина);
+
+ // Ждать завершения задания нельзя: обработчик завершения потока успел бы отработать целиком,
+ // вместе со сном наблюдателя, и мерить было бы нечего. Ждем сам сон.
+ Ожидаем.Что(ДождатьсяСигнала(СигналПаузы), "Наблюдатель дождался брошенной транзакции").ЭтоИстина();
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+ ДождатьсяЗадания(Задание);
+
+ Захваты = Наблюдатель.Завершенные("Соединение", "Занять");
+ Последний = Захваты[Захваты.ВГраница()];
+
+ Ожидаем.Что(Последний.Длительность(), "Спящий наблюдатель не держит блокировку пула").Меньше(300);
+
+КонецПроцедуры
+
+// Транзакции
+
+&Тест
+Процедура ФиксацияДаетСобытиеТранзакцииСИсходомCommit() Экспорт
+
+ МенеджерСущностей.НачатьТранзакцию();
+
+ Ожидаем.Что(Наблюдатель.Начатые("Транзакция")).ИмеетДлину(1);
+ Ожидаем.Что(Наблюдатель.Завершенные("Транзакция")).ИмеетДлину(0);
+
+ МенеджерСущностей.Сохранить(НовыйПростойОбъект(1));
+ МенеджерСущностей.ЗафиксироватьТранзакцию();
+
+ Транзакции = Наблюдатель.Завершенные("Транзакция");
+ Ожидаем.Что(Транзакции).ИмеетДлину(1);
+ Ожидаем.Что(Транзакции[0].Исход()).Равно("commit");
+ Ожидаем.Что(Транзакции[0].Ошибка()).Равно(Неопределено);
+ Ожидаем.Что(Транзакции[0].Длительность()).Не_().Меньше(0);
+ Ожидаем.Что(Транзакции[0], "В обеих фазах один объект").Равно(Наблюдатель.Начатые("Транзакция")[0]);
+
+КонецПроцедуры
+
+&Тест
+Процедура ОтменаДаетСобытиеТранзакцииСИсходомRollback() Экспорт
+
+ МенеджерСущностей.НачатьТранзакцию();
+ МенеджерСущностей.ОтменитьТранзакцию();
+
+ Транзакции = Наблюдатель.Завершенные("Транзакция");
+ Ожидаем.Что(Транзакции).ИмеетДлину(1);
+ Ожидаем.Что(Транзакции[0].Исход()).Равно("rollback");
+
+КонецПроцедуры
+
+&Тест
+Процедура ОшибкаЗавершенияДаетИсходFailed() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ Настройки = Новый НастройкиКоннектораТранзакций;
+ Настройки.ПадатьПриЗавершенииТранзакции = Истина;
+ СоздатьМенеджер(10, Настройки);
+
+ МенеджерСущностей.НачатьТранзакцию();
+
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("ЗафиксироватьТранзакцию", Новый Массив)
+ .ВыбрасываетИсключение("Завершение транзакции не удалось");
+
+ Транзакции = Наблюдатель.Завершенные("Транзакция");
+ Ожидаем.Что(Транзакции).ИмеетДлину(1);
+ Ожидаем.Что(Транзакции[0].Исход()).Равно("failed");
+ Ожидаем.Что(Транзакции[0].Ошибка()).ИмеетТип("ИнформацияОбОшибке");
+
+КонецПроцедуры
+
+&Тест
+Процедура ОшибкаНачалаТранзакцииДаетИсходFailed() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ Настройки = Новый НастройкиКоннектораТранзакций;
+ Настройки.ПадатьПриНачалеТранзакции = Истина;
+ СоздатьМенеджер(10, Настройки);
+
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("НачатьТранзакцию", Новый Массив)
+ .ВыбрасываетИсключение("Начало транзакции не удалось");
+
+ Транзакции = Наблюдатель.Завершенные("Транзакция");
+ Ожидаем.Что(Транзакции, "Событие начато до BEGIN и завершено его ошибкой").ИмеетДлину(1);
+ Ожидаем.Что(Транзакции[0].Исход()).Равно("failed");
+ Ожидаем.Что(Транзакции[0].Ошибка()).ИмеетТип("ИнформацияОбОшибке");
+
+КонецПроцедуры
+
+&Тест
+Процедура БрошеннаяТранзакцияЗавершаетсяИсходомAbandoned() Экспорт
+
+ Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ОборватьТранзакциюВФоне", Новый Массив, Истина);
+ ДождатьсяЗадания(Задание);
+
+ Ожидаем.Что(Задание.ИнформацияОбОшибке <> Неопределено, "Задание должно было оборваться").ЭтоИстина();
+
+ // Соединение возвращается обработчиком завершения потока; ему дается время сработать.
+ Транзакции = ДождатьсяЗавершенныхТранзакций(1);
+
+ Ожидаем.Что(Транзакции).ИмеетДлину(1);
+ Ожидаем.Что(Транзакции[0].Исход()).Равно("abandoned");
+
+КонецПроцедуры
+
+&Тест
+Процедура ОтвергнутоеВложенноеНачалоНеОбрываетСобытиеВнешнейТранзакции() Экспорт
+
+ МенеджерСущностей.Закрыть();
+ Настройки = Новый НастройкиКоннектораТранзакций;
+ Настройки.ОтвергатьВложеннуюТранзакцию = Истина;
+ СоздатьМенеджер(10, Настройки);
+
+ МенеджерСущностей.НачатьТранзакцию();
+
+ Ожидаем.Что(МенеджерСущностей)
+ .Метод("НачатьТранзакцию", Новый Массив)
+ .ВыбрасываетИсключение("Вложенная транзакция не поддерживается");
+
+ Завершенные = Наблюдатель.Завершенные("Транзакция");
+ Ожидаем.Что(Завершенные, "Отвергнутое начало завершает только свое событие").ИмеетДлину(1);
+ Ожидаем.Что(Завершенные[0].Исход()).Равно("failed");
+
+ МенеджерСущностей.ЗафиксироватьТранзакцию();
+
+ Начатые = Наблюдатель.Начатые("Транзакция");
+ Ожидаем.Что(Начатые, "Каждое начало транзакции - свое событие").ИмеетДлину(2);
+
+ Завершенные = Наблюдатель.Завершенные("Транзакция");
+ Ожидаем.Что(Завершенные, "Событие внешней транзакции дожило до фиксации").ИмеетДлину(2);
+ Ожидаем.Что(Завершенные[1].Исход()).Равно("commit");
+ Ожидаем.Что(Завершенные[1], "Зафиксировано событие внешней транзакции").Равно(Начатые[0]);
+
+КонецПроцедуры
+
+&Тест
+Процедура ЗавершениеЗакрываетВерхнееСобытиеСтопкиТранзакций() Экспорт
+
+ МенеджерСущностей.НачатьТранзакцию();
+ МенеджерСущностей.НачатьТранзакцию();
+
+ Начатые = Наблюдатель.Начатые("Транзакция");
+ Ожидаем.Что(Начатые, "Вложенное начало дает свое событие").ИмеетДлину(2);
+ Ожидаем.Что(Наблюдатель.Завершенные("Транзакция")).ИмеетДлину(0);
+
+ МенеджерСущностей.ЗафиксироватьТранзакцию();
+
+ Завершенные = Наблюдатель.Завершенные("Транзакция");
+ Ожидаем.Что(Завершенные).ИмеетДлину(2);
+ Ожидаем.Что(Завершенные[0].Исход(), "Фиксация закрывает верхнее событие стопки").Равно("commit");
+ Ожидаем.Что(Завершенные[0]).Равно(Начатые[1]);
+ Ожидаем.Что(Завершенные[1].Исход(), "Остаток стопки брошен возвратом соединения").Равно("abandoned");
+ Ожидаем.Что(Завершенные[1]).Равно(Начатые[0]);
+
+КонецПроцедуры
+
+&Тест
+Процедура БрошеннаяСтопкаТранзакцийЗавершаетсяЦеликом() Экспорт
+
+ Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ОборватьВложеннуюТранзакциюВФоне", Новый Массив, Истина);
+ ДождатьсяЗадания(Задание);
+
+ Ожидаем.Что(Задание.ИнформацияОбОшибке <> Неопределено, "Задание должно было оборваться").ЭтоИстина();
+
+ // Соединение возвращается обработчиком завершения потока; ему дается время сработать.
+ Транзакции = ДождатьсяЗавершенныхТранзакций(2);
+
+ Ожидаем.Что(Транзакции, "Брошены оба начала").ИмеетДлину(2);
+ Ожидаем.Что(Транзакции[0].Исход()).Равно("abandoned");
+ Ожидаем.Что(Транзакции[1].Исход()).Равно("abandoned");
+
+КонецПроцедуры
+
+// Фоновые процедуры
+
+// Держит соединение транзакцией, пока основной поток пытается его занять.
+//
+Процедура ДержатьСоединениеВФоне() Экспорт
+ МенеджерСущностей.НачатьТранзакцию();
+ Приостановить(400);
+ МенеджерСущностей.ЗафиксироватьТранзакцию();
+КонецПроцедуры
+
+// Начинает транзакцию и обрывается, не завершив ее.
+//
+Процедура ОборватьТранзакциюВФоне() Экспорт
+ МенеджерСущностей.НачатьТранзакцию();
+ ВызватьИсключение "Обрыв фонового задания";
+КонецПроцедуры
+
+// Портит соединение неудачной фиксацией и возвращает его в пул.
+//
+Процедура ИспортитьСоединениеВФоне() Экспорт
+
+ МенеджерСущностей.НачатьТранзакцию();
+
+ Попытка
+ МенеджерСущностей.ЗафиксироватьТранзакцию();
+ Исключение
+ // Ошибка фиксации и нужна: после нее соединение непригодно, и пул его закроет.
+ ОшибкаФиксацииВФоне = ИнформацияОбОшибке();
+ КонецПопытки;
+
+КонецПроцедуры
+
+// Начинает вложенную транзакцию и обрывается, не завершив ни одной.
+//
+Процедура ОборватьВложеннуюТранзакциюВФоне() Экспорт
+ МенеджерСущностей.НачатьТранзакцию();
+ МенеджерСущностей.НачатьТранзакцию();
+ ВызватьИсключение "Обрыв фонового задания";
+КонецПроцедуры
+
+// Служебные
+
+// Создает источник с тестовым коннектором транзакций, менеджер на нем и наблюдателя.
+//
+// Параметры:
+// РазмерПула - Число - Предел соединений источника.
+// Настройки - НастройкиКоннектораТранзакций, Неопределено - Поведение коннектора.
+// Неопределено - безотказная база.
+//
+Процедура СоздатьМенеджер(Знач РазмерПула, Знач Настройки = Неопределено)
+
+ Если Настройки = Неопределено Тогда
+ Настройки = Новый НастройкиКоннектораТранзакций;
+ КонецЕсли;
+
+ Источник = Новый ИсточникДанных(
+ "соединения",
+ Тип("КоннекторТранзакцийДляТестов"),
+ "соединения",
+ Настройки.ПараметрыКоннектора(),
+ РазмерПула
+ );
+ СозданныеИсточники.Добавить(Источник);
+ // Подготовка и тест идут в разных потоках: с автозакрытием источник умер бы до теста.
+ Источник.УстановитьАвтоЗакрытие(Ложь);
+ Наблюдатель = Новый НаблюдательЗаписывающий;
+ Источник.ДобавитьНаблюдателя(Наблюдатель);
+
+ МенеджерСущностей = Новый МенеджерСущностей(Источник);
+ МенеджерСущностей.ДобавитьКлассВМодель(Тип("ПростойОбъект"));
+ МенеджерСущностей.ДобавитьКлассВМодель(Тип("СущностьБезГенерируемогоИдентификатора"));
+ МенеджерСущностей.ДобавитьКлассВМодель(Тип("Автор"));
+
+ МенеджерСущностей.Инициализировать();
+
+КонецПроцедуры
+
+// Ждет отметку сигнала, не дольше 5 секунд.
+//
+// Параметры:
+// Сигнал - СигналСобытия - Сигнал, который ставит проверяемый код.
+//
+// Возвращаемое значение:
+// Булево - Истина, если отметка появилась; Ложь, если не дождались.
+//
+Функция ДождатьсяСигнала(Сигнал)
+
+ КрайнийСрок = ТекущаяУниверсальнаяДатаВМиллисекундах() + 5000;
+
+ Пока НЕ Сигнал.Отмечен() Цикл
+ Если ТекущаяУниверсальнаяДатаВМиллисекундах() > КрайнийСрок Тогда
+ Возврат Ложь;
+ КонецЕсли;
+ Приостановить(5);
+ КонецЦикла;
+
+ Возврат Истина;
+
+КонецФункции
+
+Функция ДождатьсяЗавершенныхТранзакций(Знач Количество)
+ Возврат ДождатьсяТранзакций("Завершенные", Количество);
+КонецФункции
+
+// Ждет, пока наблюдатель не увидит нужное число завершенных освобождений соединения,
+// не дольше 5 секунд.
+//
+// Параметры:
+// Количество - Число - Сколько событий ждать.
+//
+// Возвращаемое значение:
+// Массив из СобытиеИсточникаДанных - События освобождения, сколько бы их ни набралось к сроку.
+//
+Функция ДождатьсяОсвобождений(Знач Количество)
+
+ КрайнийСрок = ТекущаяУниверсальнаяДатаВМиллисекундах() + 5000;
+
+ Пока Истина Цикл
+ Освобождения = Наблюдатель.Завершенные("Соединение", "Освободить");
+ Если Освобождения.Количество() >= Количество ИЛИ ТекущаяУниверсальнаяДатаВМиллисекундах() > КрайнийСрок Тогда
+ Возврат Освобождения;
+ КонецЕсли;
+ Приостановить(20);
+ КонецЦикла;
+
+КонецФункции
+
+Функция ДождатьсяНачатыхТранзакций(Знач Количество)
+ Возврат ДождатьсяТранзакций("Начатые", Количество);
+КонецФункции
+
+// Ждет, пока наблюдатель не увидит нужное число событий транзакций указанной фазы, не дольше 5 секунд.
+//
+// Параметры:
+// Фаза - Строка - "Начатые" или "Завершенные": метод наблюдателя, которым отбираются события.
+// Количество - Число - Сколько событий ждать.
+//
+// Возвращаемое значение:
+// Массив из СобытиеИсточникаДанных - События транзакций, сколько бы их ни набралось к сроку.
+//
+Функция ДождатьсяТранзакций(Знач Фаза, Знач Количество)
+
+ КрайнийСрок = ТекущаяУниверсальнаяДатаВМиллисекундах() + 5000;
+
+ Пока Истина Цикл
+ Если Фаза = "Начатые" Тогда
+ Транзакции = Наблюдатель.Начатые("Транзакция");
+ Иначе
+ Транзакции = Наблюдатель.Завершенные("Транзакция");
+ КонецЕсли;
+ Если Транзакции.Количество() >= Количество ИЛИ ТекущаяУниверсальнаяДатаВМиллисекундах() > КрайнийСрок Тогда
+ Возврат Транзакции;
+ КонецЕсли;
+ Приостановить(20);
+ КонецЦикла;
+
+КонецФункции
+
+Процедура ДождатьсяЗадания(Задание)
+ Если НЕ Задание.ОжидатьЗавершения(30000) Тогда
+ ВызватьИсключение "Фоновое задание не завершилось за 30 секунд";
+ КонецЕсли;
+КонецПроцедуры
+
+// Колонка идентификатора ПростойОбъект строковая: без &Колонка тип колонки - Строка.
+//
+// Параметры:
+// Идентификатор - Строка, Число - Идентификатор объекта, приводится к строке.
+//
+// Возвращаемое значение:
+// ПростойОбъект - Объект с заполненными идентификатором и полем.
+//
+Функция НовыйПростойОбъект(Знач Идентификатор)
+ Объект = Новый ПростойОбъект;
+ Объект.Идентификатор = Строка(Идентификатор);
+ Объект.Поле = "поле " + Идентификатор;
+ Возврат Объект;
+КонецФункции
+
+Процедура ПодключитьФикстуры()
+
+ Фикстуры = Новый Массив;
+ Фикстуры.Добавить("ПростойОбъект");
+ Фикстуры.Добавить("СущностьБезГенерируемогоИдентификатора");
+ Фикстуры.Добавить("Автор");
+ Фикстуры.Добавить("НаблюдательЗаписывающий");
+ Фикстуры.Добавить("СигналСобытия");
+ Фикстуры.Добавить("НаблюдательТормозящий");
+ Фикстуры.Добавить("ПланЗакрытийКоннектора");
+ Фикстуры.Добавить("НастройкиКоннектораТранзакций");
+ Фикстуры.Добавить("КоннекторТранзакцийДляТестов");
+
+ Для Каждого Фикстура Из Фикстуры Цикл
+ ПодключитьСценарий(
+ ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", Фикстура + ".os"),
+ Фикстура
+ );
+ КонецЦикла;
+
+КонецПроцедуры
diff --git "a/tests/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" "b/tests/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os"
index e746521f..ff730ddc 100644
--- "a/tests/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os"
+++ "b/tests/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os"
@@ -217,6 +217,87 @@
КонецПроцедуры
+// Пул сущностей держит по одному экземпляру на идентификатор, и сохранение обязано положить
+// туда сохраненный объект. Возврат соединения к этому отношения не имеет: он идет после работы
+// с СУБД и упасть может на закрытии непригодного соединения, но сохраненная сущность к тому
+// моменту уже своя.
+//
+&Тест
+Процедура НеудачноеОсвобождениеНеЛишаетСохраненнуюСущностьТождества() Экспорт
+
+ ПодключитьФикстурыПадающегоЗакрытия();
+ ХранилищеВПамяти.Очистить("тождество");
+
+ Настройки = Новый НастройкиКоннектораТранзакций;
+
+ // Пул без предела соединений не держит и закрывает соединение на каждом возврате - только
+ // так неудачное закрытие попадает в освобождение соединения операции.
+ ИсточникТождества = Новый ИсточникДанных(
+ "тождество",
+ Тип("КоннекторТранзакцийДляТестов"),
+ "тождество",
+ Настройки.ПараметрыКоннектора(),
+ 0
+ );
+ ИсточникТождества.УстановитьАвтоЗакрытие(Ложь);
+
+ Менеджер = ТестовыеУтилиты.МенеджерАвторов(ИсточникТождества);
+
+ Попытка
+
+ ХранилищеАвторов = Менеджер.ПолучитьХранилищеСущностей(Тип("Автор"));
+
+ // Настройки коннектор читает при открытии, поэтому план достается соединению, которое
+ // займет сохранение: закрытия инициализации уже позади и по плану не считаются.
+ Настройки.ПланЗакрытий = Новый ПланЗакрытийКоннектора(1, 1);
+
+ Автор = Новый Автор;
+ Автор.Имя = "Пушкин";
+
+ Попытка
+ ХранилищеАвторов.Сохранить(Автор);
+ ОшибкаОсвобождения = Неопределено;
+ Исключение
+ ОшибкаОсвобождения = ИнформацияОбОшибке();
+ КонецПопытки;
+
+ Ожидаем.Что(ОшибкаОсвобождения, "Освобождение соединения падает на закрытии")
+ .Не_().Равно(Неопределено);
+
+ Найденный = ХранилищеАвторов.ПолучитьОдно(Автор.ВнутреннийИдентификатор);
+
+ Ожидаем.Что(Найденный = Автор, "Чтение отдает сохраненный экземпляр, а не второй на тот же идентификатор")
+ .ЭтоИстина();
+
+ Исключение
+ Менеджер.Закрыть();
+ ИсточникТождества.Закрыть();
+ ВызватьИсключение;
+ КонецПопытки;
+
+ Менеджер.Закрыть();
+ ИсточникТождества.Закрыть();
+
+КонецПроцедуры
+
+// Подключает фикстуры коннектора, чье закрытие падает по плану.
+//
+Процедура ПодключитьФикстурыПадающегоЗакрытия()
+
+ Фикстуры = Новый Массив;
+ Фикстуры.Добавить("ПланЗакрытийКоннектора");
+ Фикстуры.Добавить("НастройкиКоннектораТранзакций");
+ Фикстуры.Добавить("КоннекторТранзакцийДляТестов");
+
+ Для Каждого Фикстура Из Фикстуры Цикл
+ ПодключитьСценарий(
+ ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", Фикстура + ".os"),
+ Фикстура
+ );
+ КонецЦикла;
+
+КонецПроцедуры
+
// Создает источник и менеджер, работает с сущностью и закрывает их.
//
Процедура ПрожитьЦиклМенеджера()