├── .settings.php
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── admin
├── dashboard.php
└── menu.php
├── assets
├── components
│ ├── AppRefreshPicker.vue
│ ├── AppTimePicker.vue
│ ├── ChartPanel.vue
│ ├── DashboardPanel.vue
│ ├── MessageDetails.vue
│ ├── RecentMessagesPanel.vue
│ └── StatPanel.vue
├── i18n.js
├── index.js
├── locales
│ └── ru.js
├── plugins
│ └── element-ui.js
├── styles
│ ├── _settings.scss
│ ├── components
│ │ ├── _dashboard-panel.scss
│ │ ├── _dashboard.scss
│ │ ├── _drawer.scss
│ │ ├── _link.scss
│ │ ├── _message-details.scss
│ │ ├── _refresh-picker.scss
│ │ ├── _stat-panel.scss
│ │ ├── _table-panel.scss
│ │ ├── _table.scss
│ │ ├── _time-picker.scss
│ │ └── index.scss
│ ├── index.scss
│ └── vendor
│ │ ├── _element-ui.scss
│ │ ├── _vue-pretty-json.scss
│ │ └── index.scss
├── utils
│ └── request.js
└── views
│ ├── Dashboard.vue
│ ├── ViewMixin.js
│ └── index.js
├── bin
└── console
├── composer.json
├── config
├── messenger.xml
└── monitoring.xml
├── default_option.php
├── docs
├── README.md
├── configuration.md
├── creating-message-handlers.md
├── events.md
├── getting-started.md
├── images
│ ├── dashboard.png
│ └── message_details.png
├── monitoring-adapters-registration.md
├── monitoring.md
├── supervisor-configuration.md
└── transports-registration.md
├── include.php
├── install
├── admin
│ └── bsi_queue_dashboard.php
├── db
│ └── mysql
│ │ ├── install.sql
│ │ └── uninstall.sql
├── index.php
├── js
│ └── bsi.queue
│ │ ├── 447.70e017ad.js
│ │ ├── 447.70e017ad.js.LICENSE.txt
│ │ ├── 484.ce118c7d.js
│ │ ├── 484.ce118c7d.js.LICENSE.txt
│ │ ├── 57.c2262f4a.css
│ │ ├── 57.ed404a44.js
│ │ ├── 57.ed404a44.js.LICENSE.txt
│ │ ├── app.301a11ab.js
│ │ ├── app.f2bb9a23.css
│ │ ├── entrypoints.json
│ │ ├── fonts
│ │ ├── element-icons.313f7dac.woff
│ │ └── element-icons.45201881.ttf
│ │ └── manifest.json
├── step1.php
├── themes
│ └── .default
│ │ ├── bsi.queue.css
│ │ └── icons
│ │ └── bsi.queue
│ │ └── menu.svg
├── unstep1.php
├── unstep2.php
└── version.php
├── lang
└── ru
│ ├── admin
│ ├── dashboard.php
│ └── menu.php
│ ├── install
│ └── index.php
│ └── options.php
├── lib
├── cache
│ ├── bitrixcacheadapter.php
│ └── cacheitem.php
├── event
│ ├── syncmessagefailedevent.php
│ └── syncmessagehandledevent.php
├── exception
│ ├── invalidargumentexception.php
│ ├── logicexception.php
│ └── runtimeexception.php
├── middleware
│ └── adduuidstampmiddleware.php
├── monitoring
│ ├── adapter
│ │ ├── adapterfactory.php
│ │ ├── adapterfactoryinterface.php
│ │ ├── adapterinterface.php
│ │ └── bitrix
│ │ │ ├── bitrixadapter.php
│ │ │ ├── bitrixadapterfactory.php
│ │ │ └── bitrixmessagestattable.php
│ ├── agent
│ │ └── cleanupstatsagent.php
│ ├── consumercounter.php
│ ├── controller
│ │ ├── abstractcontroller.php
│ │ └── dashboard.php
│ ├── eventlistener
│ │ └── pushstatslistener.php
│ ├── messagestats.php
│ ├── messagestatscollection.php
│ ├── messagestatuses.php
│ ├── repository
│ │ ├── bitrixmessagestatsrepository.php
│ │ └── messagestatsrepositoryinterface.php
│ ├── stamp
│ │ └── ignoremonitoringstamp.php
│ └── storage
│ │ ├── bitrixstorage.php
│ │ └── storageinterface.php
├── queue.php
├── queueevents.php
├── stamp
│ └── uuidstamp.php
├── transport
│ └── bitrix
│ │ ├── bitrixreceivedstamp.php
│ │ ├── bitrixreceiver.php
│ │ ├── bitrixsender.php
│ │ ├── bitrixtransport.php
│ │ ├── bitrixtransportfactory.php
│ │ ├── connection.php
│ │ └── messagetable.php
└── utils
│ ├── chartintervalcalculator.php
│ └── webpackencoreloader.php
├── options.php
├── package.json
├── phpcs.xml.dist
├── prolog.php
├── webpack.config.js
└── yarn.lock
/.settings.php:
--------------------------------------------------------------------------------
1 | [
5 | 'value' => [
6 | 'namespaces' => [
7 | 'Bsi\\Queue\\Monitoring\\Controller' => 'api',
8 | ],
9 | ],
10 | 'readonly' => true,
11 | ],
12 | ];
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## [1.1.1] - 2025-04-23
4 | ### Changed
5 | - Баг #64: Исправлена ошибка обязательности HEADERS и баг с транзакциями mysql
6 | - Поправлена регистрация хэндлера
7 |
8 | ## [1.1.0] - 2024-02-03
9 | ### Changed
10 | - Изменена конфигурация DI контейнера под symfony/messenger 5.3+
11 | - Изменён способ парсинга конфигурации для транспорта bitrix
12 |
13 | ## [1.0.1] - 2023-10-06
14 | ### Changed
15 | - Обновлена зависимость psr/container:~2.0.0
16 |
17 | ## [1.0.0] - 2023-10-06
18 | ### Changed
19 | - Обновлена зависимость psr/container:~1.1.0
20 |
21 | ## [0.11.3] - 2023-03-30
22 | ### Added
23 | - Поддержка composer/installers v2
24 |
25 | ## [0.11.2] - 2022-03-18
26 | ### Fixed
27 | - Изменено требование к версии symfony/console (#44)
28 |
29 | ## [0.11.1] - 2022-01-20
30 | ### Fixed
31 | - Исправлена ошибка при использовании кэша контейнера (#38)
32 | - Исправление уязвимостей в зависимостях
33 |
34 | ## [0.11.0] - 2021-08-03
35 | ### Added
36 | - Добавлена зависимость `psr/container:~1.0.0` для устранения конфликта с битриксовой версией пакета
37 | ### Fixed
38 | - Исправлена ошибка `ServiceNotFoundException`
39 |
40 | ## [0.10.0] - 2021-07-23
41 | ### Added
42 | - Добавлен штамп `IgnoreMonitoringStamp` позволяющий исключить сообщение из отслеживания мониторингом
43 | ### Fixed
44 | - Обновлены зависимости
45 |
46 | ## [0.9.4] - 2020-12-26
47 | ### Added
48 | - Добавлена возможность кэширования скомпилированного DI-контейнера с помощью метода `useCache`. По умолчанию: выключено.
49 | - Добавлена возможность изменять кол-во выводимых сообщений в мониторинге.
50 | - Добавлена возможность фильтрации сообщений в мониторинге.
51 | ### Fixed
52 | - Исправлены ошибки с некорректным отображением статуса обработки "синхронных" сообщений.
53 |
54 | ## [0.9.3] - 2020-08-25
55 | ### Fixed
56 | - Исправлена ошибка с обработчиками событий и командой `stop-workers`
57 |
58 | ## [0.9.2] - 2020-07-27
59 | ### Fixed
60 | - Исправлена ошибка с подсчетом метрик в мониторинге
61 |
62 | ## [0.9.1] - 2020-07-27
63 | ### Fixed
64 | - Исправлена ошибка с пустым графиком в мониторинге
65 |
66 | ## [0.9.0] - 2020-07-26
67 | - Первый релиз
68 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Работа над проектом
2 |
3 | Перед отправкой `pull request` проверьте:
4 | - Ваш код должен соответствовать стандарту [PSR-12: Extended Coding Style](https://www.php-fig.org/psr/psr-12/). Запустите `composer list` для проверки кода и `composer fix` для автоматического устанения ошибок.
5 | - Unit-тесты должны выполнится успешно. Запустите `composer test` для их выполнения.
6 | - Создавайте как можно больше unit-тестов.
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Sergey Balasov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | # Модуль очередей
9 |
10 | Модуль очередей для 1С-Битрикс. Позволяет отложено обрабатывать команды из приложения.
11 |
12 | Модуль является "мостом" для компонента [symfony/messenger](https://symfony.com/doc/current/messenger.html).
13 |
14 | ## Документация
15 |
16 | О том, как установить и использовать модуль читайте в [официальной документации](https://bsidev.github.io/bitrix-queue/).
17 |
18 | ## Лицензия
19 |
20 | Bitrix Queue распространяется по лицензии [MIT](LICENSE)
--------------------------------------------------------------------------------
/admin/dashboard.php:
--------------------------------------------------------------------------------
1 | IsAdmin()) {
13 | $APPLICATION->AuthForm(Loc::getMessage('ACCESS_DENIED'));
14 | }
15 |
16 | require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_after.php');
17 |
18 | $APPLICATION->SetTitle(Loc::getMessage('BSI_QUEUE_ADMIN_DASHBOARD_TITLE'));
19 |
20 | (new WebpackEncoreLoader())->load('app');
21 |
22 | echo '';
23 |
24 | require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/epilog_admin.php');
25 |
--------------------------------------------------------------------------------
/admin/menu.php:
--------------------------------------------------------------------------------
1 | IsAdmin()) {
9 | return false;
10 | }
11 |
12 | if (!Loader::includeModule('bsi.queue')) {
13 | return false;
14 | }
15 |
16 | return [
17 | 'parent_menu' => 'global_menu_settings',
18 | 'section' => 'bsi_queue',
19 | 'sort' => 2000,
20 | 'text' => Loc::getMessage('BSI_QUEUE_MENU_TEXT'),
21 | 'title' => Loc::getMessage('BSI_QUEUE_MENU_TITLE'),
22 | 'icon' => 'bsi_queue_menu_icon',
23 | 'page_icon' => 'bsi_queue_page_icon',
24 | 'items_id' => 'menu_bsi_queue',
25 | 'items' => [
26 | [
27 | 'text' => Loc::getMessage('BSI_QUEUE_MENU_DASHBOARD_TEXT'),
28 | 'title' => Loc::getMessage('BSI_QUEUE_MENU_DASHBOARD_TITLE'),
29 | 'url' => 'bsi_queue_dashboard.php?lang=' . LANGUAGE_ID,
30 | 'more_url' => ['bsi_queue_dashboard.php'],
31 | ],
32 | ],
33 | ];
34 |
--------------------------------------------------------------------------------
/assets/components/AppRefreshPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/assets/components/AppTimePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ currentPreset.name }}
6 |
7 |
8 |
9 |
14 | {{ preset.name }}
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/assets/components/ChartPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/assets/components/DashboardPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
Нет данных
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/assets/components/MessageDetails.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
17 |
18 | {{ prop.name }}
19 |
20 |
21 | {{ prop.value }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
38 |
42 |
43 |
44 |
45 |
46 |
50 |
51 | {{ row }}
52 | |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/assets/components/RecentMessagesPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 | {{ $t('label.sent_at') }}
14 | |
15 |
16 | {{ $t('label.message') }}
17 | |
18 |
19 | {{ $t('label.status') }}
20 | |
21 |
22 | {{ $t('label.transport_name') }}
23 | |
24 |
25 | {{ $t('label.buses') }}
26 | |
27 |
28 |
29 |
30 |
34 |
35 | {{ formatDate(row.sent_at) }}
36 | |
37 |
38 |
42 | {{ row.message }}
43 |
44 | |
45 |
46 |
50 | {{ $t(`enums.status.${row.status}`) }}
51 |
52 | |
53 |
54 | {{ row.transport_name }}
55 | |
56 |
57 | {{ Array.isArray(row.buses) ? row.buses.join(', ') : '' }}
58 | |
59 |
60 |
61 |
62 |
66 | Нет данных
67 | |
68 |
69 |
70 |
71 |
72 |
81 |
82 |
83 |
84 |
170 |
--------------------------------------------------------------------------------
/assets/components/StatPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/assets/i18n.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueI18n from 'vue-i18n';
3 |
4 | Vue.use(VueI18n);
5 |
6 | const loadLocales = () => {
7 | const locales = require.context(
8 | './locales',
9 | true,
10 | /[A-Za-z0-9-_,\s]+\.js$/i
11 | );
12 | const messages = {};
13 | locales.keys().forEach(key => {
14 | const matched = key.match(/([A-Za-z0-9-_]+)\./i);
15 | if (matched && matched.length > 1) {
16 | const locale = matched[1];
17 | messages[locale] = locales(key).default;
18 | }
19 | });
20 | return messages;
21 | };
22 |
23 | export default new VueI18n({
24 | locale: 'ru',
25 | fallbackLocale: 'ru',
26 | messages: loadLocales()
27 | });
--------------------------------------------------------------------------------
/assets/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | import './plugins/element-ui';
4 | import i18n from './i18n';
5 |
6 | import views from './views';
7 | import './styles/index.scss';
8 |
9 | Vue.config.productionTip = false;
10 |
11 | document.addEventListener('DOMContentLoaded', () => {
12 | const nodes = Array.from(document.querySelectorAll('.vue-shell'));
13 | nodes.forEach(node => {
14 | let initialData = node.dataset['initial'];
15 | if (initialData !== undefined) {
16 | try {
17 | initialData = JSON.parse(initialData);
18 | } catch (e) {
19 | console.warn(e);
20 | }
21 | }
22 |
23 | if (views[node.dataset['name']] !== undefined) {
24 | new Vue({
25 | el: node,
26 | i18n,
27 | render(h) {
28 | return h(views[node.dataset['name']], {
29 | props: { initial: initialData }
30 | });
31 | }
32 | });
33 | }
34 | });
35 | });
--------------------------------------------------------------------------------
/assets/locales/ru.js:
--------------------------------------------------------------------------------
1 | export default {
2 | label: {
3 | uuid: 'UUID',
4 | message: 'Сообщение',
5 | status: 'Статус',
6 | sent_at: 'Дата отправки',
7 | received_at: 'Дата получения',
8 | handled_at: 'Дата обработки',
9 | failed_at: 'Дата ошибки',
10 | transport_name: 'Получатель',
11 | buses: 'Шины',
12 | problem: 'Проблема',
13 | ok: 'ОК',
14 | findMessages: 'Поиск сообщений'
15 | },
16 | title: {
17 | status: 'Статус',
18 | consumers: 'Подписчики',
19 | stats: 'Статистика',
20 | messages: 'Сообщения',
21 | summary: 'Сводка',
22 | data: 'Данные',
23 | errors: 'Ошибки'
24 | },
25 | tooltip: {
26 | autoUpdate: 'Автоматическое обновление',
27 | notFoundConsumers: 'Не обнаружено активных подписчиков'
28 | },
29 | enums: {
30 | status: {
31 | sent: 'Отправлено',
32 | received: 'Получено',
33 | handled: 'Обработано',
34 | failed: 'С ошибками'
35 | },
36 | datePreset: {
37 | last5m: 'Последние 5 минут',
38 | last15m: 'Последние 15 минут',
39 | last30m: 'Последние 30 минут',
40 | last1h: 'Последний 1 час',
41 | last3h: 'Последние 3 часа',
42 | last6h: 'Последние 6 часов',
43 | last12h: 'Последние 12 часов',
44 | last24h: 'Последние 24 часа',
45 | last2d: 'Последние 2 дня',
46 | last7d: 'Последние 7 дней',
47 | last30d: 'Последние 30 дней',
48 | last60d: 'Последние 60 дней',
49 | last90d: 'Последние 90 дней',
50 | last6M: 'Последние 6 месяцев',
51 | last1y: 'Последний 1 год'
52 | }
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/assets/plugins/element-ui.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import {
3 | Col,
4 | Row,
5 | Button,
6 | Dropdown,
7 | DropdownMenu,
8 | DropdownItem,
9 | Pagination,
10 | Tag,
11 | Drawer,
12 | Input,
13 | Select,
14 | Option
15 | } from 'element-ui';
16 | import lang from 'element-ui/lib/locale/lang/ru-RU';
17 | import locale from 'element-ui/lib/locale';
18 |
19 | locale.use(lang);
20 |
21 | Vue.prototype.$ELEMENT = { size: 'small' };
22 | Vue.use(Row);
23 | Vue.use(Col);
24 | Vue.use(Button);
25 | Vue.use(Dropdown);
26 | Vue.use(DropdownMenu);
27 | Vue.use(DropdownItem);
28 | Vue.use(Pagination);
29 | Vue.use(Tag);
30 | Vue.use(Drawer);
31 | Vue.use(Input);
32 | Vue.use(Select);
33 | Vue.use(Option);
34 |
--------------------------------------------------------------------------------
/assets/styles/_settings.scss:
--------------------------------------------------------------------------------
1 | $color-primary: #2675d7 !default;
2 | $color-success: #67c23a !default;
3 | $color-warning: #e6a23c !default;
4 | $color-danger: #f56c6c !default;
5 | $color-info: #909399 !default;
6 |
7 | $color-text-primary: #333 !default;
8 | $color-text-regular: #535c69 !default;
9 | $color-text-secondary: #a3a9b1 !default;
10 |
11 | $color-link-default: $color-primary !default;
12 |
13 | $color-bg-gray: #fafafa !default;
--------------------------------------------------------------------------------
/assets/styles/components/_dashboard-panel.scss:
--------------------------------------------------------------------------------
1 | .c-dashboard-panel {
2 | position: relative;
3 | display: flex;
4 | flex-direction: column;
5 | width: 100%;
6 | height: 100%;
7 | border-radius: 4px;
8 | background-color: #fff;
9 | box-shadow: 0 1px 1px 0 rgba(0, 0, 0, .04);
10 |
11 | &__header {
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | box-sizing: border-box;
16 | height: 40px;
17 | padding: 0 15px;
18 | }
19 |
20 | &__title {
21 | font-size: 16px;
22 | color: $color-text-regular;
23 | }
24 |
25 | &__content {
26 | flex-grow: 1;
27 | box-sizing: border-box;
28 | width: 100%;
29 | height: calc(100% - 40px);
30 | }
31 | }
--------------------------------------------------------------------------------
/assets/styles/components/_dashboard.scss:
--------------------------------------------------------------------------------
1 | .c-dashboard {
2 | &__panel {
3 | display: flex;
4 | justify-content: flex-end;
5 | margin: -41px -5px 20px 0;
6 | }
7 |
8 | & > .el-row {
9 | margin-bottom: 20px;
10 | }
11 | }
--------------------------------------------------------------------------------
/assets/styles/components/_drawer.scss:
--------------------------------------------------------------------------------
1 | .c-drawer {
2 | outline: 0;
3 | background-color: #e4edef !important;
4 |
5 | .el-drawer {
6 | &__header {
7 | margin-bottom: 20px;
8 | color: $color-text-primary;
9 |
10 | span {
11 | font-size: 16px;
12 | }
13 | }
14 |
15 | &__body {
16 | overflow-y: auto;
17 | }
18 | }
19 |
20 | [role] {
21 | outline: 0;
22 | }
23 | }
--------------------------------------------------------------------------------
/assets/styles/components/_link.scss:
--------------------------------------------------------------------------------
1 | .c-link {
2 | cursor: pointer;
3 | text-decoration: none;
4 | color: $color-link-default;
5 |
6 | &:hover, &:active, &:focus, &:visited {
7 | color: $color-link-default;
8 | }
9 |
10 | &:hover {
11 | text-decoration: underline;
12 | }
13 | }
--------------------------------------------------------------------------------
/assets/styles/components/_message-details.scss:
--------------------------------------------------------------------------------
1 | .c-message-details {
2 |
3 |
4 | padding: 0 20px;
5 |
6 | &__summary {
7 | padding: 0 10px;
8 |
9 | .el-row {
10 | padding: 10px 0;
11 |
12 | &:nth-child(odd) {
13 | background-color: $color-bg-gray;
14 | }
15 | }
16 |
17 | .el-col:first-child {
18 | color: $color-text-secondary;
19 | }
20 | }
21 |
22 | &__data {
23 | height: 100%;
24 | background-color: $color-bg-gray;
25 | }
26 | }
--------------------------------------------------------------------------------
/assets/styles/components/_refresh-picker.scss:
--------------------------------------------------------------------------------
1 | .c-refresh-picker {
2 | margin: 0 5px;
3 | }
--------------------------------------------------------------------------------
/assets/styles/components/_stat-panel.scss:
--------------------------------------------------------------------------------
1 | .c-stat-panel {
2 | &__value {
3 | font-size: 30px;
4 | line-height: 1;
5 | box-sizing: border-box;
6 | padding: 10px 15px 15px;
7 | text-align: center;
8 | color: $color-text-primary;
9 | }
10 |
11 | &--success &__value {
12 | color: $color-success;
13 | }
14 |
15 | &--warning &__value {
16 | color: $color-warning;
17 | }
18 |
19 | &--danger &__value {
20 | color: $color-danger;
21 | }
22 |
23 | &--info &__value {
24 | color: $color-info;
25 | }
26 | }
--------------------------------------------------------------------------------
/assets/styles/components/_table-panel.scss:
--------------------------------------------------------------------------------
1 | .c-table-panel {
2 | &__search {
3 | padding: 5px 10px;
4 |
5 | .el-input__inner {
6 | line-height: 26px !important;
7 | height: 28px !important;
8 | border: 1px solid #dcdfe6 !important;
9 | box-shadow: none !important;
10 | }
11 | }
12 |
13 | .el-pagination {
14 | padding: 5px;
15 | text-align: center;
16 |
17 | &__sizes {
18 | margin-right: -135px;
19 | }
20 |
21 | .el-select {
22 | .el-input {
23 | width: 135px;
24 |
25 | &__inner {
26 | height: 28px !important;
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/assets/styles/components/_table.scss:
--------------------------------------------------------------------------------
1 | .c-table {
2 | width: 100%;
3 | border-collapse: collapse;
4 |
5 | td, th {
6 | padding: 10px;
7 | text-align: left;
8 | border-bottom: 1px solid #ebeef5;
9 | background-color: #fff;
10 | }
11 |
12 | th {
13 | font-weight: 700;
14 | color: $color-text-secondary;
15 | }
16 |
17 | td {
18 | color: $color-text-regular;
19 | }
20 |
21 | &--striped tbody tr:nth-child(odd) td {
22 | background-color: $color-bg-gray;
23 | }
24 | }
--------------------------------------------------------------------------------
/assets/styles/components/_time-picker.scss:
--------------------------------------------------------------------------------
1 | .c-time-picker {
2 | margin: 0 5px;
3 | }
--------------------------------------------------------------------------------
/assets/styles/components/index.scss:
--------------------------------------------------------------------------------
1 | @import "link";
2 | @import "table";
3 | @import "time-picker";
4 | @import "refresh-picker";
5 | @import "dashboard";
6 | @import "dashboard-panel";
7 | @import "stat-panel";
8 | @import "table-panel";
9 | @import "drawer";
10 | @import "message-details";
--------------------------------------------------------------------------------
/assets/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import "settings";
2 | @import "vendor/index";
3 | @import "components/index";
--------------------------------------------------------------------------------
/assets/styles/vendor/_element-ui.scss:
--------------------------------------------------------------------------------
1 | $--color-primary: $color-primary;
2 | $--font-path: '~element-ui/lib/theme-chalk/fonts';
3 |
4 | @import "~element-ui/packages/theme-chalk/src/base";
5 | @import "~element-ui/packages/theme-chalk/src/common/popup";
6 | @import "~element-ui/packages/theme-chalk/src/button";
7 | @import "~element-ui/packages/theme-chalk/src/col";
8 | @import "~element-ui/packages/theme-chalk/src/drawer";
9 | @import "~element-ui/packages/theme-chalk/src/dropdown-item";
10 | @import "~element-ui/packages/theme-chalk/src/dropdown-menu";
11 | @import "~element-ui/packages/theme-chalk/src/dropdown";
12 | @import "~element-ui/packages/theme-chalk/src/pagination";
13 | @import "~element-ui/packages/theme-chalk/src/row";
14 | @import "~element-ui/packages/theme-chalk/src/col";
15 | @import "~element-ui/packages/theme-chalk/src/input";
16 | @import "~element-ui/packages/theme-chalk/src/select";
17 | @import "~element-ui/packages/theme-chalk/src/select-dropdown";
18 |
--------------------------------------------------------------------------------
/assets/styles/vendor/_vue-pretty-json.scss:
--------------------------------------------------------------------------------
1 | .vjs-tree {
2 | background-color: $color-bg-gray !important;
3 |
4 | .vjs-key {
5 | color: #333 !important;
6 | }
7 |
8 | .vjs-value {
9 | &__string {
10 | color: #d14 !important;
11 | }
12 |
13 | &__number {
14 | color: teal !important;
15 | }
16 | }
17 |
18 | &.is-root {
19 | padding: .5em;
20 | }
21 | }
--------------------------------------------------------------------------------
/assets/styles/vendor/index.scss:
--------------------------------------------------------------------------------
1 | @import "element-ui";
2 | @import "vue-pretty-json";
--------------------------------------------------------------------------------
/assets/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import qs from 'qs';
3 | import BX from 'bitrix';
4 |
5 | const service = axios.create({
6 | timeout: 30000
7 | });
8 |
9 | service.interceptors.request.use(
10 | config => {
11 | config.headers['X-Bitrix-Csrf-Token'] = BX.bitrix_sessid();
12 |
13 | return config;
14 | },
15 | error => {
16 | console.error(error);
17 | return Promise.reject(error);
18 | }
19 | );
20 |
21 | service.interceptors.response.use(
22 | ({ data }) => {
23 | if (typeof data === 'object' && data.status === 'error') {
24 | if (Array.isArray(data.errors) && data.errors.length > 0) {
25 | return Promise.reject(new Error(
26 | data.errors
27 | .map(error => error.message)
28 | .join('\n')
29 | ));
30 | }
31 |
32 | return Promise.reject(new Error('Error'));
33 | } else {
34 | return data.data;
35 | }
36 | },
37 | error => {
38 | console.error(error);
39 | return Promise.reject(error);
40 | }
41 | );
42 |
43 | export const fetchFromModule = (endpoint, data = {}) => {
44 | if (typeof endpoint !== 'string' || endpoint.length === 0) {
45 | throw new TypeError('Invalid argument "endpoint"');
46 | }
47 |
48 | return service({
49 | url: '/bitrix/services/main/ajax.php',
50 | method: 'post',
51 | params: { action: endpoint },
52 | data: qs.stringify(data, { arrayFormat: 'indices' })
53 | });
54 | };
55 |
--------------------------------------------------------------------------------
/assets/views/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
18 |
19 |
20 |
21 | -
22 |
23 |
28 | {{ hasConsumers ? $t('label.ok') : $t('label.problem') }}
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ summary.consumers ? summary.consumers.toLocaleString() : 0 }}
36 |
37 |
38 |
39 |
40 |
41 |
42 | {{ summary.sent ? summary.sent.toLocaleString() : 0 }}
43 |
44 |
45 |
46 |
47 |
48 |
49 | {{ summary.received ? summary.received.toLocaleString() : 0 }}
50 |
51 |
52 |
53 |
54 |
55 |
56 | {{ summary.handled ? summary.handled.toLocaleString() : 0 }}
57 |
58 |
59 |
60 |
61 |
62 |
63 | {{ summary.failed ? summary.failed.toLocaleString() : 0 }}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
93 |
94 |
95 |
96 |
97 |
106 |
109 |
110 |
111 |
112 |
113 |
291 |
--------------------------------------------------------------------------------
/assets/views/ViewMixin.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | initial: {
4 | type: Object,
5 | default: () => {
6 | return {};
7 | }
8 | }
9 | }
10 | };
--------------------------------------------------------------------------------
/assets/views/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | Dashboard: () => import('./Dashboard')
3 | };
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getContainer();
30 | $application = new Application();
31 | $commandLoader = $container->get('console.command_loader');
32 | if ($commandLoader instanceof CommandLoaderInterface) {
33 | $application->setCommandLoader($commandLoader);
34 | }
35 | $application->run();
36 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bsidev/bitrix-queue",
3 | "description": "Queues for Bitrix CMS",
4 | "keywords": [
5 | "bitrix",
6 | "queue"
7 | ],
8 | "type": "bitrix-d7-module",
9 | "license": "MIT",
10 | "support": {
11 | "issues": "https://github.com/bsidev/bitrix-queue/issues",
12 | "source": "https://github.com/bsidev/bitrix-queue"
13 | },
14 | "authors": [
15 | {
16 | "name": "Sergey Balasov",
17 | "email": "sbalasov@gmail.com"
18 | }
19 | ],
20 | "extra": {
21 | "installer-name": "bsi.queue"
22 | },
23 | "require": {
24 | "php": ">=7.2.5|^8.0",
25 | "ext-json": "*",
26 | "composer/installers": "^1.0|^2.0",
27 | "psr/cache": "~1.0.0",
28 | "psr/container": "~2.0.0",
29 | "ramsey/uuid": "^3.0|^4.0",
30 | "symfony/config": "^4.4.17|^5.1.9|^6.0|^7.0",
31 | "symfony/console": "^4.1|^5.0|^6.0|^7.0",
32 | "symfony/dependency-injection": "^4.4.17|^5.1.9|^6.0|^7.0",
33 | "symfony/event-dispatcher": "^4.4|^5.0|^6.0|^7.0",
34 | "symfony/messenger": "^5.3|^6.0"
35 | },
36 | "require-dev": {
37 | "ext-redis": "*",
38 | "mockery/mockery": "^1.3",
39 | "phpunit/phpunit": "^8.5",
40 | "roave/security-advisories": "dev-latest",
41 | "squizlabs/php_codesniffer": "^3.5",
42 | "symfony/property-access": "^4.4|^5.0|^6.0",
43 | "symfony/redis-messenger": "^5.1|^6.0",
44 | "symfony/serializer": "^4.4|^5.0|^6.0",
45 | "symfony/var-dumper": "^4.4|^5.0|^6.0"
46 | },
47 | "autoload-dev": {
48 | "psr-4": {
49 | "Bsi\\Queue\\Tests\\": "tests/"
50 | }
51 | },
52 | "suggest": {
53 | "symfony/amqp-messenger": "Provides AMQP integration for Symfony Messenger",
54 | "symfony/redis-messenger": "Provides Redis integration for Symfony Messenger"
55 | },
56 | "scripts": {
57 | "test": "phpunit",
58 | "lint": "phpcs",
59 | "fix": "phpcbf"
60 | },
61 | "config": {
62 | "sort-packages": true,
63 | "allow-plugins": {
64 | "composer/installers": true,
65 | "php-http/discovery": true
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/config/messenger.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
--------------------------------------------------------------------------------
/config/monitoring.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/default_option.php:
--------------------------------------------------------------------------------
1 | 365,
5 | ];
6 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Введение
2 |
3 | Модуль очередей для 1С-Битрикс. Позволяет отложено обрабатывать команды из приложения.
4 |
5 | Модуль является "мостом" для компонента [symfony/messenger](https://symfony.com/doc/current/messenger.html).
6 |
7 | ## Возможности
8 |
9 | - Поддержка почти всех возможностей оригинального компонента.
10 | - Дополнительный "транспорт" `bitrix://` для передачи сообщений через Bitrix ORM.
11 | - Возможность вносить правки в конфигурацию модуля извне посредством обработчиков событий.
12 | - Мониторинг очередей с дашбордом.
13 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | # Конфигурация
2 |
3 | Настройки выполняются в файле `/bitrix/.settings.php` или `/bitrix/.settings_extra.php`.
4 |
5 | ## Пример конфигурации
6 |
7 | ```php
8 | // bitrix/.settings_extra.php
9 | return [
10 | // ...
11 | 'bsi.queue' => [
12 | 'value' => [
13 | 'buses' => ['command_bus', 'query_bus'],
14 | 'default_bus' => 'command_bus',
15 | 'transports' => [
16 | 'sync' => 'sync://',
17 | 'async' => [
18 | 'dsn' => 'redis://redis:6379/messages',
19 | 'retry_strategy' => [
20 | 'max_retries' => 3,
21 | 'multiplier' => 2,
22 | ],
23 | ],
24 | 'failed' => 'bitrix://default?queue_name=failed',
25 | ],
26 | 'failure_transport' => 'failed',
27 | 'routing' => [
28 | 'App\Message\TestMessage' => 'async',
29 | ],
30 | 'monitoring' => [
31 | 'enabled' => true,
32 | 'adapter' => 'bitrix',
33 | 'buses' => ['command_bus'],
34 | ],
35 | ],
36 | 'readonly' => true,
37 | ],
38 | // ...
39 | ];
40 | ```
41 |
42 | ## Параметры
43 |
44 | - [buses](#buses)
45 | - [default_bus](#default_bus)
46 | - [transports](#transports)
47 | - [failure_transport](#failure_transport)
48 | - [routing](#routing)
49 | - [monitoring](#monitoring-enabled)
50 |
51 | ### buses
52 |
53 | - Тип: `array`
54 | - По умолчанию:
55 | ```php
56 | [
57 | 'buses' => [
58 | 'default' => [
59 | 'default_middleware' => true,
60 | 'middleware' => [],
61 | ],
62 | ],
63 | ];
64 | ```
65 |
66 | Шины для передачи сообщений.
67 |
68 | ### default_bus
69 |
70 | - Тип: `string`
71 | - По умолчанию: `null`
72 |
73 | Имя шина по умолчанию. При наличии более одной шины - **обязательно** (иначе, автоматически выбирается первая).
74 |
75 | ### transports
76 |
77 | - Тип: `array`
78 | - По умолчанию: `[]`
79 |
80 | Транспорты для отправки и получения сообщений.
81 |
82 | ### failure_transport
83 |
84 | - Тип: `string`
85 | - По умолчанию: `null`
86 |
87 | Имя транспорта для отправки и получения неудачных сообщений.
88 | По умолчанию, ошибочные сообщения повторно обрабатываются несколько раз (`max_retries`) и после этого "отбрасываются". С помощью `failure_transport` можно перенаправить такие сообщения в отдельный транспорт для повторной обработки.
89 |
90 | Пример:
91 |
92 | ```php
93 | [
94 | 'failure_transport' => 'failed',
95 | 'transports' => [
96 | 'async' => [
97 | 'dsn' => 'redis://redis:6379/messages',
98 | 'retry_strategy' => [
99 | 'max_retries' => 3,
100 | 'multiplier' => 2,
101 | ],
102 | ],
103 | 'failed' => 'bitrix://default?queue_name=failed',
104 | ],
105 | ];
106 | ```
107 |
108 | ### routing
109 |
110 | - Тип: `array`
111 | - По умолчанию: `[]`
112 |
113 | Маршрутизация сообщений в нужный транспорт. Можно указать несколько транспортов для одного сообщения.
114 |
115 | Пример:
116 |
117 | ```php
118 | [
119 | 'routing' => [
120 | 'App\Message\AbstractAsyncMessage' => 'async',
121 | 'App\Message\AsyncMessageInterface' => 'async',
122 | 'My\Message\ToBeSentToTwoSenders' => ['async', 'audit'],
123 | ],
124 | ];
125 | ```
126 |
127 | ### monitoring.enabled
128 |
129 | - Тип: `bool`
130 | - По умолчанию: `true`
131 |
132 | Активность мониторинга.
133 |
134 | ### monitoring.adapter
135 |
136 | - Тип: `string`
137 | - По умолчанию: `'bitrix'`
138 |
139 | Адаптер для хранения и вывода статистики.
140 |
141 | ### monitoring.buses
142 |
143 | - Тип: `array`
144 | - По умолчанию: `[]`
145 |
146 | Имена шин, которые отслеживает мониторинг. Если передан пустой массив, то отслеживаются все шины.
147 |
148 | **Ссылки по теме:**
149 |
150 | - [Настройка параметров ядра](https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&LESSON_ID=2795)
151 | - [The Messenger Component](https://symfony.com/doc/current/components/messenger.html)
152 | - [Messenger: Sync & Queued Message Handling](https://symfony.com/doc/current/messenger.html)
--------------------------------------------------------------------------------
/docs/creating-message-handlers.md:
--------------------------------------------------------------------------------
1 | # Создание обработчиков
2 |
3 | Для обработки сообщения необходимы два класса:
4 | - Класс-сообщение, который содержит необходимые данные.
5 | - Класс-обработчик, который занимается обработкой сообщения.
6 |
7 | Класс-обработчик должен реализовывать интерфейс `Symfony\Component\Messenger\Handler\MessageHandlerInterface` и иметь метод `__invoke()`, который принимает в качестве входного параметра объект класса-сообщения.
8 |
9 | ## Регистрация обработчика
10 |
11 | Обработчики регистрируются с помощью метода `addMessageHandler`
12 |
13 | Пример:
14 |
15 | ```php
16 | addMessageHandler(MyMessageHandler::class);
25 | $queue->boot();
26 | }
27 | ```
28 |
29 | ::: warning ВАЖНО
30 | Обработчики должны добавляться до инициализации системы очередей (вызова метода `boot()`).
31 | :::
32 |
33 | Пример отправки сообщения:
34 |
35 | ```php
36 | dispatchMessage(new MyMessage('Hello, world'));
41 | ```
42 |
43 | **Ссылки по теме:**
44 |
45 | - [Messenger: Sync & Queued Message Handling](https://symfony.com/doc/current/messenger.html)
46 |
--------------------------------------------------------------------------------
/docs/events.md:
--------------------------------------------------------------------------------
1 | # События
2 |
3 | ### QueueEvents::LOAD_CONFIGURATION
4 |
5 | Позволяет вносить изменения в конфигурацию динамически, без прямой правки файла конфигурации.
6 |
7 | Пример использования:
8 |
9 | ```php
10 | // local/php_interface/init.php
11 |
12 | use Bitrix\Main\Event;
13 | use Bitrix\Main\EventManager;
14 | use Bitrix\Main\EventResult;
15 | use Bitrix\Main\Loader;
16 | use Bsi\Queue\Queue;
17 | use Bsi\Queue\QueueEvents;
18 |
19 | EventManager::getInstance()->addEventHandler('', QueueEvents::LOAD_CONFIGURATION, function(Event $event) {
20 | return new EventResult(EventResult::SUCCESS, [
21 | 'routing' => [
22 | 'App\Message\TestMessage' => 'async',
23 | ],
24 | ]);
25 | });
26 |
27 | if (Loader::includeModule('bsi.queue')) {
28 | $queue = Queue::getInstance();
29 | $queue->boot();
30 | }
31 | ```
32 |
33 | **Ссылки по теме:**
34 |
35 | - [EventManager](https://dev.1c-bitrix.ru/api_d7/bitrix/main/EventManager/index.php)
36 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Начало работы
2 |
3 | ## Установка
4 |
5 | ::: warning Требования
6 | - PHP >=7.2.5
7 | - 1С-Битрикс >=17.5.10
8 | - composer/installers ^1.0
9 | :::
10 |
11 | 1. Настройте пути установки модулей в `composer.json`:
12 |
13 | ```json
14 | {
15 | "extra": {
16 | "installer-paths": {
17 | "bitrix/modules/{$name}/": [
18 | "type:bitrix-d7-module"
19 | ]
20 | }
21 | }
22 | }
23 | ```
24 |
25 | > Указывается путь до папки `bitrix/modules` относительно файла `composer.json`.
26 |
27 | 2. Установите модуль через [Composer](https://getcomposer.org/):
28 |
29 | ```sh
30 | composer require bsidev/bitrix-queue
31 | ```
32 |
33 | 3. Перейдите в раздел Marketplace административной панели и установите модуль следуя инструкциям.
34 |
35 | ```
36 | http://домен/bitrix/admin/partner_modules.php?id=bsi.queue&lang=ru&install=Y
37 | ```
38 |
39 | ## Настройка
40 |
41 | Проинициализируйте ядро модуля:
42 |
43 | ```php
44 | // local/php_interface/init.php
45 |
46 | // ...
47 |
48 | use Bitrix\Main\Loader;
49 | use Bsi\Queue\Queue;
50 |
51 | if (Loader::includeModule('bsi.queue')) {
52 | $queue = Queue::getInstance();
53 | $queue->boot();
54 | }
55 | ```
56 |
57 | ## Запуск воркера
58 |
59 | Для запуска обработки сообщений используется консольный скрипт:
60 |
61 | ```
62 | php bitrix/modules/bsi.queue/bin/console messenger:consume async --time-limit=3600
63 | ```
64 |
65 | [Consuming Messages (Running the Worker)](https://symfony.com/doc/current/messenger.html#consuming-messages-running-the-worker)
66 |
--------------------------------------------------------------------------------
/docs/images/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bsidev/bitrix-queue/fd4e78a87a9a55eca34923c1da37ed695cfd3c13/docs/images/dashboard.png
--------------------------------------------------------------------------------
/docs/images/message_details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bsidev/bitrix-queue/fd4e78a87a9a55eca34923c1da37ed695cfd3c13/docs/images/message_details.png
--------------------------------------------------------------------------------
/docs/monitoring-adapters-registration.md:
--------------------------------------------------------------------------------
1 | # Регистрация адаптера мониторинга
2 |
3 | TODO
--------------------------------------------------------------------------------
/docs/monitoring.md:
--------------------------------------------------------------------------------
1 | # Мониторинг
2 |
3 | Модуль включает в себя мониторинг очередей с дашбордом. Мониторинг позволяет отслеживать основные показатели очередей и выводить детальную информацию по обработке каждого сообщения.
4 |
5 | 
6 |
7 | 
8 |
9 | ## Конфигурация
10 |
11 | Параметры конфигурации мониторинга описаны в разделе [Конфигурация](configuration.html#monitoring-enabled).
12 |
13 | ## Очистка
14 |
15 | По умолчанию, статистика по сообщениям хранится **365 дней**. Изменить это значение можно в настройках модуля.
16 |
--------------------------------------------------------------------------------
/docs/supervisor-configuration.md:
--------------------------------------------------------------------------------
1 | # Конфигурация Supervisor
2 |
3 | Supervisor - это серверная утилита, которая позволяет контролировать процессы-воркеры. Она автоматически перезапускает процессы в случае ошибок или удачного завершения, позволяет масштабировать процессы и др.
4 |
5 | Пример установки пакета в ОС CentOS:
6 |
7 | ```
8 | yum install supervisor
9 | ```
10 |
11 | Пример файла конфигурации:
12 |
13 | ```ini
14 | ;/etc/supervisor.d/messenger-worker.ini
15 | [program:messenger-worker]
16 | directory=/home/bitrix/www
17 | command=php bitrix/modules/bsi.queue/bin/console messenger:consume async --time-limit=3600
18 | user=bitrix
19 | numprocs=2
20 | startsecs=0
21 | autostart=true
22 | autorestart=true
23 | process_name=%(program_name)s_%(process_num)02d
24 | ```
25 |
26 | ::: warning Внимание
27 | При внесении правок в обработчики сообщений необходимо перезапустить все процессы-воркеры. Для этого можно воспользоваться командой:
28 |
29 | ```
30 | php bitrix/modules/bsi.queue/bin/console messenger:stop-workers
31 | ```
32 |
33 | Она ждет успешной обработки последней итерации и останавливает процесс. Затем Supervisor создаст новые рабочие процессы.
34 | :::
35 |
36 | **Ссылки по теме:**
37 |
38 | - [Supervisor configuration](https://symfony.com/doc/current/messenger.html#supervisor-configuration)
39 |
--------------------------------------------------------------------------------
/docs/transports-registration.md:
--------------------------------------------------------------------------------
1 | # Регистрация транспортов
2 |
3 | ::: warning ВАЖНО
4 | Начиная с версии `5.1` пакета `symfony/messenger` транспорты AMQP, Redis и Doctrine вынесены в отдельные пакеты и в будущем будут удалены из основного пакета.
5 | :::
6 |
7 | ## Регистрация транспорта на примере Redis
8 |
9 | ### Composer
10 |
11 | ```sh
12 | composer require symfony/redis-messenger
13 | ```
14 |
15 | ### Регистрация фабрики
16 |
17 | ```php
18 | registerTransportFactory('redis', RedisTransportFactory::class);
27 | $queue->boot();
28 | }
29 | ```
30 |
31 | ### Пример конфигурации
32 |
33 | ```php
34 | [
35 | // ...
36 | 'transports' => [
37 | 'async' => [
38 | 'dsn' => 'redis://redis:6379/messages',
39 | ],
40 | ],
41 | // ...
42 | ];
43 | ```
44 |
45 | ::: warning ВАЖНО
46 | Обработчики должны добавляться до инициализации системы очередей (вызова метода `boot()`).
47 | :::
48 |
49 | **Ссылки по теме:**
50 |
51 | - [Messenger: Sync & Queued Message Handling](https://symfony.com/doc/current/messenger.html)
--------------------------------------------------------------------------------
/include.php:
--------------------------------------------------------------------------------
1 | MODULE_VERSION = $arModuleVersion['VERSION'] ?? null;
31 | $this->MODULE_VERSION_DATE = $arModuleVersion['VERSION_DATE'] ?? null;
32 | }
33 |
34 | $this->MODULE_NAME = Loc::getMessage('BSI_QUEUE_MODULE_NAME');
35 | $this->MODULE_DESCRIPTION = Loc::getMessage('BSI_QUEUE_MODULE_DESCRIPTION');
36 | }
37 |
38 | public function doInstall(): void
39 | {
40 | global $APPLICATION;
41 |
42 | $this->installFiles();
43 | $this->installDb();
44 |
45 | Loader::includeModule($this->MODULE_ID);
46 |
47 | $APPLICATION->includeAdminFile(
48 | Loc::getMessage('BSI_QUEUE_INSTALL_TITLE'),
49 | __DIR__ . '/step1.php'
50 | );
51 | }
52 |
53 | public function doUninstall(): void
54 | {
55 | global $APPLICATION, $step;
56 |
57 | $step = (int) $step;
58 | if ($step < 2) {
59 | $GLOBALS['errors'] = [];
60 | $APPLICATION->includeAdminFile(
61 | Loc::getMessage('BSI_QUEUE_UNINSTALL_TITLE'),
62 | __DIR__ . '/unstep1.php'
63 | );
64 | } elseif ($step === 2) {
65 | $this->uninstallDb([
66 | 'savedata' => $_REQUEST['savedata'],
67 | ]);
68 | $this->uninstallFiles();
69 |
70 | $GLOBALS['errors'] = [];
71 | $APPLICATION->includeAdminFile(
72 | Loc::getMessage('BSI_QUEUE_UNINSTALL_TITLE'),
73 | __DIR__ . '/unstep2.php'
74 | );
75 | }
76 | }
77 |
78 | public function installDb(): bool
79 | {
80 | global $APPLICATION, $DB;
81 |
82 | $this->errors = $DB->RunSQLBatch(__DIR__ . '/db/' . strtolower($DB->type) . '/install.sql');
83 | if (!empty($this->errors)) {
84 | $APPLICATION->ThrowException(implode('', $this->errors));
85 |
86 | return false;
87 | }
88 |
89 | ModuleManager::registerModule($this->MODULE_ID);
90 |
91 | $startTime = ConvertTimeStamp(time() + \CTimeZone::GetOffset() + 60, 'FULL');
92 | CAgent::AddAgent(
93 | CleanUpStatsAgent::class . '::run();',
94 | $this->MODULE_ID,
95 | 'N',
96 | 86400,
97 | '',
98 | 'Y',
99 | $startTime,
100 | 100,
101 | false,
102 | false
103 | );
104 |
105 | return true;
106 | }
107 |
108 | public function uninstallDb($params = []): bool
109 | {
110 | global $APPLICATION, $DB;
111 |
112 | $this->errors = false;
113 | if (!$params['savedata']) {
114 | $this->errors = $DB->RunSQLBatch(__DIR__ . '/db/' . strtolower($DB->type) . '/uninstall.sql');
115 | }
116 |
117 | if (!empty($this->errors)) {
118 | $APPLICATION->ThrowException(implode('', $this->errors));
119 |
120 | return false;
121 | }
122 |
123 | if (!$params['savedata']) {
124 | Option::delete($this->MODULE_ID);
125 | }
126 |
127 | CAgent::RemoveModuleAgents($this->MODULE_ID);
128 |
129 | ModuleManager::unRegisterModule($this->MODULE_ID);
130 |
131 | return true;
132 | }
133 |
134 | public function installFiles(): bool
135 | {
136 | CopyDirFiles(__DIR__ . '/admin', $_SERVER['DOCUMENT_ROOT'] . '/bitrix/admin');
137 | CopyDirFiles(__DIR__ . '/js', $_SERVER['DOCUMENT_ROOT'] . '/bitrix/js', true, true);
138 | CopyDirFiles(__DIR__ . '/themes', $_SERVER['DOCUMENT_ROOT'] . '/bitrix/themes', true, true);
139 |
140 | return true;
141 | }
142 |
143 | public function uninstallFiles(): bool
144 | {
145 | DeleteDirFiles(__DIR__ . '/admin', $_SERVER['DOCUMENT_ROOT'] . '/bitrix/admin');
146 | DeleteDirFiles(__DIR__ . '/js', $_SERVER['DOCUMENT_ROOT'] . '/bitrix/js');
147 | DeleteDirFiles(__DIR__ . '/themes/.default/', $_SERVER['DOCUMENT_ROOT'] . '/bitrix/themes/.default/');
148 | DeleteDirFilesEx('/bitrix/themes/.default/icons/bsi.queue/');
149 |
150 | return true;
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/install/js/bsi.queue/447.70e017ad.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
2 |
--------------------------------------------------------------------------------
/install/js/bsi.queue/484.ce118c7d.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * ApexCharts v3.44.0
3 | * (c) 2018-2023 ApexCharts
4 | * Released under the MIT License.
5 | */
6 |
7 | /*!
8 | * php-unserialize-js JavaScript Library
9 | * https://github.com/bd808/php-unserialize-js
10 | *
11 | * Copyright 2013 Bryan Davis and contributors
12 | * Released under the MIT license
13 | * http://www.opensource.org/licenses/MIT
14 | */
15 |
16 | /*! svg.draggable.js - v2.2.2 - 2019-01-08
17 | * https://github.com/svgdotjs/svg.draggable.js
18 | * Copyright (c) 2019 Wout Fierens; Licensed MIT */
19 |
20 | /*! svg.filter.js - v2.0.2 - 2016-02-24
21 | * https://github.com/wout/svg.filter.js
22 | * Copyright (c) 2016 Wout Fierens; Licensed MIT */
23 |
--------------------------------------------------------------------------------
/install/js/bsi.queue/57.ed404a44.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * Vue.js v2.7.14
3 | * (c) 2014-2022 Evan You
4 | * Released under the MIT License.
5 | */
6 |
7 | /*!
8 | * vue-i18n v8.28.2
9 | * (c) 2022 kazuya kawaguchi
10 | * Released under the MIT License.
11 | */
12 |
--------------------------------------------------------------------------------
/install/js/bsi.queue/app.301a11ab.js:
--------------------------------------------------------------------------------
1 | (()=>{var e,t,r={5540:(e,t,r)=>{var n={"./ru.js":6038};function a(e){var t=o(e);return r(t)}function o(e){if(!r.o(n,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return n[e]}a.keys=function(){return Object.keys(n)},a.resolve=o,e.exports=a,a.id=5540},767:(e,t,r)=>{"use strict";r(1038),r(8783),r(9554),r(1539),r(4747),r(7941),r(8309);var n=r(144),a=r(5614),o=r.n(a),s=r(7186),i=r.n(s),u=r(7626),l=r.n(u),d=r(7665),c=r.n(d),f=r(7698),v=r.n(f),p=r(7787),m=r.n(p),h=r(2173),b=r.n(h),y=r(3229),g=r.n(y),P=r(7099),O=r.n(P),Z=r(6426),k=r.n(Z),w=r(1530),j=r.n(w),E=r(905),_=r.n(E),T=r(9840);r(1802).default.use(T.Z),n.ZP.prototype.$ELEMENT={size:"small"},n.ZP.use(_()),n.ZP.use(j()),n.ZP.use(k()),n.ZP.use(O()),n.ZP.use(g()),n.ZP.use(b()),n.ZP.use(m()),n.ZP.use(v()),n.ZP.use(c()),n.ZP.use(l()),n.ZP.use(i()),n.ZP.use(o());r(6992),r(3948),r(4916),r(4723);var C=r(7152);n.ZP.use(C.Z);const x=new C.Z({locale:"ru",fallbackLocale:"ru",messages:(M=r(5540),L={},M.keys().forEach((function(e){var t=e.match(/([A-Za-z0-9-_]+)\./i);if(t&&t.length>1){var r=t[1];L[r]=M(e).default}})),L)});var M,L;r(8674);const N={Dashboard:function(){return Promise.all([r.e(484),r.e(447)]).then(r.bind(r,6099))}};n.ZP.config.productionTip=!1,document.addEventListener("DOMContentLoaded",(function(){Array.from(document.querySelectorAll(".vue-shell")).forEach((function(e){var t=e.dataset.initial;if(void 0!==t)try{t=JSON.parse(t)}catch(e){console.warn(e)}void 0!==N[e.dataset.name]&&new n.ZP({el:e,i18n:x,render:function(r){return r(N[e.dataset.name],{props:{initial:t}})}})}))}))},6038:(e,t,r)=>{"use strict";r.r(t),r.d(t,{default:()=>n});const n={label:{uuid:"UUID",message:"Сообщение",status:"Статус",sent_at:"Дата отправки",received_at:"Дата получения",handled_at:"Дата обработки",failed_at:"Дата ошибки",transport_name:"Получатель",buses:"Шины",problem:"Проблема",ok:"ОК",findMessages:"Поиск сообщений"},title:{status:"Статус",consumers:"Подписчики",stats:"Статистика",messages:"Сообщения",summary:"Сводка",data:"Данные",errors:"Ошибки"},tooltip:{autoUpdate:"Автоматическое обновление",notFoundConsumers:"Не обнаружено активных подписчиков"},enums:{status:{sent:"Отправлено",received:"Получено",handled:"Обработано",failed:"С ошибками"},datePreset:{last5m:"Последние 5 минут",last15m:"Последние 15 минут",last30m:"Последние 30 минут",last1h:"Последний 1 час",last3h:"Последние 3 часа",last6h:"Последние 6 часов",last12h:"Последние 12 часов",last24h:"Последние 24 часа",last2d:"Последние 2 дня",last7d:"Последние 7 дней",last30d:"Последние 30 дней",last60d:"Последние 60 дней",last90d:"Последние 90 дней",last6M:"Последние 6 месяцев",last1y:"Последний 1 год"}}}},2640:e=>{"use strict";e.exports=BX}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var o=n[e]={exports:{}};return r[e].call(o.exports,o,o.exports,a),o.exports}a.m=r,e=[],a.O=(t,r,n,o)=>{if(!r){var s=1/0;for(d=0;d=o)&&Object.keys(a.O).every((e=>a.O[e](r[u])))?r.splice(u--,1):(i=!1,o0&&e[d-1][2]>o;d--)e[d]=e[d-1];e[d]=[r,n,o]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((t,r)=>(a.f[r](e,t),t)),[])),a.u=e=>e+"."+{447:"70e017ad",484:"ce118c7d"}[e]+".js",a.miniCssF=e=>{},a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},a.l=(e,r,n,o)=>{if(t[e])t[e].push(r);else{var s,i;if(void 0!==n)for(var u=document.getElementsByTagName("script"),l=0;l{s.onerror=s.onload=null,clearTimeout(f);var a=t[e];if(delete t[e],s.parentNode&&s.parentNode.removeChild(s),a&&a.forEach((e=>e(n))),r)return r(n)},f=setTimeout(c.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=c.bind(null,s.onerror),s.onload=c.bind(null,s.onload),i&&document.head.appendChild(s)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.p="/bitrix/js/bsi.queue/",(()=>{var e={143:0};a.f.j=(t,r)=>{var n=a.o(e,t)?e[t]:void 0;if(0!==n)if(n)r.push(n[2]);else{var o=new Promise(((r,a)=>n=e[t]=[r,a]));r.push(n[2]=o);var s=a.p+a.u(t),i=new Error;a.l(s,(r=>{if(a.o(e,t)&&(0!==(n=e[t])&&(e[t]=void 0),n)){var o=r&&("load"===r.type?"missing":r.type),s=r&&r.target&&r.target.src;i.message="Loading chunk "+t+" failed.\n("+o+": "+s+")",i.name="ChunkLoadError",i.type=o,i.request=s,n[1](i)}}),"chunk-"+t,t)}},a.O.j=t=>0===e[t];var t=(t,r)=>{var n,o,[s,i,u]=r,l=0;if(s.some((t=>0!==e[t]))){for(n in i)a.o(i,n)&&(a.m[n]=i[n]);if(u)var d=u(a)}for(t&&t(r);la(767)));o=a.O(o)})();
--------------------------------------------------------------------------------
/install/js/bsi.queue/entrypoints.json:
--------------------------------------------------------------------------------
1 | {
2 | "entrypoints": {
3 | "app": {
4 | "css": [
5 | "/bitrix/js/bsi.queue/57.c2262f4a.css",
6 | "/bitrix/js/bsi.queue/app.f2bb9a23.css"
7 | ],
8 | "js": [
9 | "/bitrix/js/bsi.queue/57.ed404a44.js",
10 | "/bitrix/js/bsi.queue/app.301a11ab.js"
11 | ]
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/install/js/bsi.queue/fonts/element-icons.313f7dac.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bsidev/bitrix-queue/fd4e78a87a9a55eca34923c1da37ed695cfd3c13/install/js/bsi.queue/fonts/element-icons.313f7dac.woff
--------------------------------------------------------------------------------
/install/js/bsi.queue/fonts/element-icons.45201881.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bsidev/bitrix-queue/fd4e78a87a9a55eca34923c1da37ed695cfd3c13/install/js/bsi.queue/fonts/element-icons.45201881.ttf
--------------------------------------------------------------------------------
/install/js/bsi.queue/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "app/app.css": "/bitrix/js/bsi.queue/app.f2bb9a23.css",
3 | "app/app.js": "/bitrix/js/bsi.queue/app.301a11ab.js",
4 | "app/447.70e017ad.js": "/bitrix/js/bsi.queue/447.70e017ad.js",
5 | "app/57.c2262f4a.css": "/bitrix/js/bsi.queue/57.c2262f4a.css",
6 | "app/57.ed404a44.js": "/bitrix/js/bsi.queue/57.ed404a44.js",
7 | "app/484.ce118c7d.js": "/bitrix/js/bsi.queue/484.ce118c7d.js",
8 | "app/fonts/element-icons.ttf": "/bitrix/js/bsi.queue/fonts/element-icons.45201881.ttf",
9 | "app/fonts/element-icons.woff": "/bitrix/js/bsi.queue/fonts/element-icons.313f7dac.woff"
10 | }
--------------------------------------------------------------------------------
/install/step1.php:
--------------------------------------------------------------------------------
1 | GetException()) {
10 | /** @noinspection PhpDynamicAsStaticMethodCallInspection */
11 | CAdminMessage::ShowMessage([
12 | 'TYPE' => 'ERROR',
13 | 'MESSAGE' => Loc::getMessage('MOD_INST_ERR'),
14 | 'DETAILS' => $ex->GetString(),
15 | 'HTML' => true,
16 | ]);
17 | } else {
18 | /** @noinspection PhpDynamicAsStaticMethodCallInspection */
19 | CAdminMessage::ShowNote(Loc::getMessage('MOD_INST_OK'));
20 | }
21 | ?>
22 |
--------------------------------------------------------------------------------
/install/themes/.default/bsi.queue.css:
--------------------------------------------------------------------------------
1 | .adm-submenu-item-link-icon.bsi_queue_menu_icon { background: url(icons/bsi.queue/menu.svg) no-repeat 50% 50%; }
--------------------------------------------------------------------------------
/install/themes/.default/icons/bsi.queue/menu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/install/unstep1.php:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/install/unstep2.php:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
18 |
--------------------------------------------------------------------------------
/install/version.php:
--------------------------------------------------------------------------------
1 | '1.1.1',
5 | 'VERSION_DATE' => '2025-04-23',
6 | ];
7 |
--------------------------------------------------------------------------------
/lang/ru/admin/dashboard.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class BitrixCacheAdapter implements CacheItemPoolInterface
14 | {
15 | /** @var int */
16 | private $lifetime;
17 | /** @var string */
18 | private $dir;
19 | /** @var Cache */
20 | private $cache;
21 | /** @var array */
22 | private $values = [];
23 |
24 | public function __construct(int $lifetime = 0, string $dir = '/bsi/queue')
25 | {
26 | $this->lifetime = $lifetime > 0 ? $lifetime : 31536000;
27 | $this->dir = $dir;
28 | /** @noinspection NullPointerExceptionInspection */
29 | $this->cache = Application::getInstance()->getCache();
30 | }
31 |
32 | /**
33 | * {@inheritdoc}
34 | */
35 | public function getItem($key): CacheItemInterface
36 | {
37 | return current($this->getItems([$key]));
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public function getItems(array $keys = []): array
44 | {
45 | $items = [];
46 |
47 | $fetched = $this->doFetch($keys);
48 | foreach ($fetched as $id => $value) {
49 | if (!isset($keys[$id])) {
50 | $id = key($keys);
51 | }
52 | $key = $keys[$id];
53 | unset($keys[$id]);
54 | $items[$key] = new CacheItem($key, $value, true);
55 | }
56 |
57 | foreach ($keys as $key) {
58 | $items[$key] = new CacheItem($key, null, false);
59 | }
60 |
61 | return $items;
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public function save(CacheItemInterface $item): bool
68 | {
69 | $this->values[$item->getKey()] = $item->get();
70 |
71 | return $this->commit();
72 | }
73 |
74 | /**
75 | * {@inheritdoc}
76 | */
77 | public function saveDeferred(CacheItemInterface $item): bool
78 | {
79 | return $this->save($item);
80 | }
81 |
82 | /**
83 | * {@inheritdoc}
84 | */
85 | public function hasItem($key): bool
86 | {
87 | return true;
88 | }
89 |
90 | /**
91 | * {@inheritdoc}
92 | */
93 | public function clear(): bool
94 | {
95 | return $this->cache->cleanDir($this->dir);
96 | }
97 |
98 | /**
99 | * {@inheritdoc}
100 | */
101 | public function deleteItem($key): bool
102 | {
103 | return $this->deleteItems([$key]);
104 | }
105 |
106 | /**
107 | * {@inheritdoc}
108 | */
109 | public function deleteItems(array $keys): bool
110 | {
111 | foreach ($keys as $key) {
112 | $this->cache->clean($key, $this->dir);
113 | }
114 |
115 | return true;
116 | }
117 |
118 | /**
119 | * {@inheritdoc}
120 | */
121 | public function commit(): bool
122 | {
123 | $this->cache->forceRewriting(true);
124 | foreach ($this->values as $key => $value) {
125 | $this->cache->startDataCache($this->lifetime, $key, $this->dir, $value);
126 | $this->cache->endDataCache();
127 | }
128 | $this->cache->forceRewriting(false);
129 |
130 | return true;
131 | }
132 |
133 | private function doFetch(array $ids): array
134 | {
135 | $values = [];
136 |
137 | foreach ($ids as $id) {
138 | if ($this->cache->initCache($this->lifetime, $id, $this->dir)) {
139 | $values[$id] = $this->cache->getVars();
140 | }
141 | }
142 |
143 | return $values;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/lib/cache/cacheitem.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | final class CacheItem implements CacheItemInterface
11 | {
12 | protected $key;
13 | protected $value;
14 | protected $isHit = false;
15 |
16 | public function __construct(string $key, $value, bool $isHit = false)
17 | {
18 | $this->key = $key;
19 | $this->value = $value;
20 | $this->isHit = $isHit;
21 | }
22 |
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function get()
27 | {
28 | return $this->value;
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function getKey(): string
35 | {
36 | return $this->key;
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function isHit(): bool
43 | {
44 | return $this->isHit;
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | public function set($value): self
51 | {
52 | $this->value = $value;
53 |
54 | return $this;
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | */
60 | public function expiresAt($expiration): self
61 | {
62 | return $this;
63 | }
64 |
65 | /**
66 | * {@inheritdoc}
67 | */
68 | public function expiresAfter($time): self
69 | {
70 | return $this;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/event/syncmessagefailedevent.php:
--------------------------------------------------------------------------------
1 | envelope = $envelope;
15 | $this->throwable = $throwable;
16 | }
17 |
18 | public function getEnvelope(): Envelope
19 | {
20 | return $this->envelope;
21 | }
22 |
23 | public function getThrowable(): \Throwable
24 | {
25 | return $this->throwable;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/event/syncmessagehandledevent.php:
--------------------------------------------------------------------------------
1 | envelope = $envelope;
14 | }
15 |
16 | public function getEnvelope(): Envelope
17 | {
18 | return $this->envelope;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/exception/invalidargumentexception.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class InvalidArgumentException extends \InvalidArgumentException
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/lib/exception/logicexception.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class LogicException extends \LogicException
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/lib/exception/runtimeexception.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class RuntimeException extends \RuntimeException
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/lib/middleware/adduuidstampmiddleware.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class AddUuidStampMiddleware implements MiddlewareInterface
14 | {
15 | public function handle(Envelope $envelope, StackInterface $stack): Envelope
16 | {
17 | if ($envelope->last(UuidStamp::class) === null) {
18 | $envelope = $envelope->with(new UuidStamp());
19 | }
20 |
21 | return $stack->next()->handle($envelope, $stack);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/monitoring/adapter/adapterfactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class AdapterFactory implements AdapterFactoryInterface
11 | {
12 | /** @var iterable|AdapterFactoryInterface[] */
13 | private $factories;
14 |
15 | /**
16 | * @param iterable|AdapterFactoryInterface[] $factories
17 | */
18 | public function __construct(iterable $factories)
19 | {
20 | $this->factories = $factories;
21 | }
22 |
23 | public function createAdapter(string $name, array $options): AdapterInterface
24 | {
25 | foreach ($this->factories as $factory) {
26 | if ($factory->supports($name, $options)) {
27 | return $factory->createAdapter($name, $options);
28 | }
29 | }
30 |
31 | throw new InvalidArgumentException(sprintf('No adapter supports the given name "%s".', $name));
32 | }
33 |
34 | public function supports(string $name, array $options): bool
35 | {
36 | foreach ($this->factories as $factory) {
37 | if ($factory->supports($name, $options)) {
38 | return true;
39 | }
40 | }
41 |
42 | return false;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/monitoring/adapter/adapterfactoryinterface.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | interface AdapterFactoryInterface
9 | {
10 | public function createAdapter(string $name, array $options): AdapterInterface;
11 |
12 | public function supports(string $name, array $options): bool;
13 | }
14 |
--------------------------------------------------------------------------------
/lib/monitoring/adapter/adapterinterface.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | interface AdapterInterface
12 | {
13 | public function getStorage(): StorageInterface;
14 |
15 | public function getMessageStatsRepository(): MessageStatsRepositoryInterface;
16 | }
17 |
--------------------------------------------------------------------------------
/lib/monitoring/adapter/bitrix/bitrixadapter.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class BitrixAdapter implements AdapterInterface
15 | {
16 | public function getStorage(): StorageInterface
17 | {
18 | return new BitrixStorage();
19 | }
20 |
21 | public function getMessageStatsRepository(): MessageStatsRepositoryInterface
22 | {
23 | return new BitrixMessageStatsRepository();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/monitoring/adapter/bitrix/bitrixadapterfactory.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class BitrixAdapterFactory implements AdapterFactoryInterface
12 | {
13 | public function createAdapter(string $name, array $options): AdapterInterface
14 | {
15 | return new BitrixAdapter();
16 | }
17 |
18 | public function supports(string $name, array $options): bool
19 | {
20 | return $name === 'bitrix';
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/monitoring/adapter/bitrix/bitrixmessagestattable.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class BitrixMessageStatTable extends DataManager
17 | {
18 | public static function getTableName(): string
19 | {
20 | return 'bsi_queue_message_stat';
21 | }
22 |
23 | public static function getMap(): array
24 | {
25 | return [
26 | (new IntegerField('ID'))
27 | ->configurePrimary(true)
28 | ->configureAutocomplete(true),
29 |
30 | (new StringField('UUID'))
31 | ->configureRequired(true)
32 | ->configureUnique(true),
33 |
34 | (new StringField('MESSAGE'))
35 | ->configureRequired(true),
36 |
37 | (new StringField('STATUS'))
38 | ->configureRequired(true),
39 |
40 | (new TextField('BODY'))
41 | ->configureRequired(true),
42 |
43 | (new ArrayField('HEADERS'))
44 | ->configureSerializationPhp(),
45 |
46 | (new StringField('TRANSPORT_NAME'))
47 | ->configureSize(190),
48 |
49 | (new TextField('ERROR')),
50 |
51 | (new DatetimeField('SENT_AT'))
52 | ->configureRequired(true)
53 | ->configureDefaultValue(static function () {
54 | return new DateTime();
55 | }),
56 |
57 | (new DatetimeField('RECEIVED_AT')),
58 |
59 | (new DatetimeField('HANDLED_AT')),
60 |
61 | (new DatetimeField('FAILED_AT')),
62 | ];
63 | }
64 |
65 | public static function getRowByUuid(string $uuid): ?array
66 | {
67 | return static::getRow([
68 | 'select' => ['*'],
69 | 'filter' => ['UUID' => $uuid],
70 | ]);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/monitoring/agent/cleanupstatsagent.php:
--------------------------------------------------------------------------------
1 | getContainer()->get(AdapterInterface::class);
17 | if ($adapter instanceof AdapterInterface) {
18 | $adapter->getStorage()->cleanUpStats($lifetime);
19 | }
20 | } catch (\Throwable $e) {
21 | }
22 |
23 | return static::class . '::run();';
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/monitoring/consumercounter.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class ConsumerCounter
9 | {
10 | /**
11 | * Returns a count of running consumers.
12 | *
13 | * @param string $command
14 | *
15 | * @return int
16 | */
17 | public function get(string $command = 'messenger:consume'): int
18 | {
19 | $output = shell_exec(sprintf('ps aux | grep -v grep | grep \'%s\'', escapeshellarg($command)));
20 |
21 | return substr_count($output ?: '', $command);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/monitoring/controller/abstractcontroller.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | abstract class AbstractController extends Controller
14 | {
15 | /** @var AdapterInterface */
16 | protected $adapter;
17 |
18 | public function __construct(Request $request = null)
19 | {
20 | parent::__construct($request);
21 |
22 | $this->adapter = Queue::getInstance()->getContainer()->get(AdapterInterface::class);
23 | }
24 |
25 | public function getAdapter(): AdapterInterface
26 | {
27 | return $this->adapter;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/monitoring/controller/dashboard.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | class Dashboard extends AbstractController
19 | {
20 | /** @var MessageStatsRepositoryInterface */
21 | private $messageStatsRepository;
22 | /** @var ConsumerCounter */
23 | private $consumerCounter;
24 |
25 | public function __construct(Request $request = null)
26 | {
27 | parent::__construct($request);
28 |
29 | $this->messageStatsRepository = $this->getAdapter()->getMessageStatsRepository();
30 | $this->consumerCounter = new ConsumerCounter();
31 | }
32 |
33 | public function summaryAction(string $from, string $to): array
34 | {
35 | $fromDate = new \DateTimeImmutable($from);
36 | $toDate = new \DateTimeImmutable($to);
37 |
38 | return [
39 | 'consumers' => $this->consumerCounter->get(),
40 | 'sent' => $this->messageStatsRepository->countSent($fromDate, $toDate),
41 | 'received' => $this->messageStatsRepository->countReceived($fromDate, $toDate),
42 | 'handled' => $this->messageStatsRepository->countHandled($fromDate, $toDate),
43 | 'failed' => $this->messageStatsRepository->countFailed($fromDate, $toDate),
44 | ];
45 | }
46 |
47 | public function queryRangeAction(string $from, string $to): ?array
48 | {
49 | $fromTs = strtotime($from);
50 | if ($fromTs === false) {
51 | $this->errorCollection->setError(new Error(sprintf('Invalid time %s', $from)));
52 | }
53 | $toTs = strtotime($to);
54 | if ($toTs === false) {
55 | $this->errorCollection->setError(new Error(sprintf('Invalid time %s', $to)));
56 | }
57 | if (!$this->errorCollection->isEmpty()) {
58 | return null;
59 | }
60 |
61 | $intervalCalculator = new ChartIntervalCalculator(200);
62 | $interval = $intervalCalculator->calculate($fromTs, $toTs);
63 |
64 | $fromTs = floor($fromTs / $interval) * $interval;
65 | $toTs = floor($toTs / $interval) * $interval;
66 |
67 | $fromDate = new \DateTimeImmutable('@' . $fromTs);
68 | $toDate = new \DateTimeImmutable('@' . $toTs);
69 |
70 | $sentDataset = $this->messageStatsRepository->getSentChartDataset($fromDate, $toDate, $interval);
71 | $receivedDataset = $this->messageStatsRepository->getReceivedChartDataset($fromDate, $toDate, $interval);
72 | $handledDataset = $this->messageStatsRepository->getHandledChartDataset($fromDate, $toDate, $interval);
73 | $failedDataset = $this->messageStatsRepository->getFailedChartDataset($fromDate, $toDate, $interval);
74 |
75 | return [
76 | ['status' => MessageStatuses::SENT, 'values' => $sentDataset],
77 | ['status' => MessageStatuses::RECEIVED, 'values' => $receivedDataset],
78 | ['status' => MessageStatuses::HANDLED, 'values' => $handledDataset],
79 | ['status' => MessageStatuses::FAILED, 'values' => $failedDataset],
80 | ];
81 | }
82 |
83 | public function recentMessagesAction(string $from, string $to, int $pageSize = 10, int $page = 1, string $search = ''): ?array
84 | {
85 | $fromDate = new \DateTimeImmutable($from);
86 | $toDate = new \DateTimeImmutable($to);
87 | $offset = ($page - 1) * $pageSize;
88 |
89 | $collection = $this->messageStatsRepository->getRecentList($fromDate, $toDate, $pageSize, $offset, $search);
90 |
91 | $data = [];
92 | /** @var MessageStats $messageStats */
93 | foreach ($collection as $messageStats) {
94 | $envelope = $messageStats->getEnvelope();
95 |
96 | $uuidStamp = $envelope->last(UuidStamp::class);
97 |
98 | $busNames = [];
99 | /** @var BusNameStamp $busNameStamp */
100 | foreach ($envelope->all(BusNameStamp::class) as $busNameStamp) {
101 | $busNames[] = $busNameStamp->getBusName();
102 | }
103 |
104 | $sentAt = $messageStats->getSentAt();
105 | $receivedAt = $messageStats->getReceivedAt();
106 | $handledAt = $messageStats->getHandledAt();
107 | $failedAt = $messageStats->getFailedAt();
108 |
109 | $data[] = [
110 | 'uuid' => $uuidStamp instanceof UuidStamp ? $uuidStamp->getUuid()->toString() : null,
111 | 'message' => get_class($envelope->getMessage()),
112 | 'data' => serialize($envelope->getMessage()),
113 | 'status' => $messageStats->getStatus(),
114 | 'transport_name' => $messageStats->getTransportName(),
115 | 'buses' => $busNames,
116 | 'error' => $messageStats->getError(),
117 | 'sent_at' => $sentAt->format(\DateTime::ATOM),
118 | 'received_at' => $receivedAt ? $receivedAt->format(\DateTime::ATOM) : null,
119 | 'handled_at' => $handledAt ? $handledAt->format(\DateTime::ATOM) : null,
120 | 'failed_at' => $failedAt ? $failedAt->format(\DateTime::ATOM) : null,
121 | ];
122 | }
123 |
124 | return [
125 | 'data' => $data,
126 | 'total' => $this->messageStatsRepository->countAll($fromDate, $toDate, $search),
127 | ];
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/lib/monitoring/eventlistener/pushstatslistener.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class PushStatsListener implements EventSubscriberInterface
23 | {
24 | /** @var AdapterInterface */
25 | private $adapter;
26 | /** @var string[] */
27 | private $busNames;
28 |
29 | public function __construct(AdapterInterface $adapter, array $busNames = [])
30 | {
31 | $this->adapter = $adapter;
32 | $this->busNames = $busNames;
33 | }
34 |
35 | public function onMessageSent(SendMessageToTransportsEvent $event): void
36 | {
37 | if (!$this->isEnvelopeEnabled($event->getEnvelope())) {
38 | return;
39 | }
40 |
41 | $this->adapter->getStorage()->pushSentMessageStats($event->getEnvelope());
42 | }
43 |
44 | public function onMessageReceived(WorkerMessageReceivedEvent $event): void
45 | {
46 | if (!$this->isEnvelopeEnabled($event->getEnvelope())) {
47 | return;
48 | }
49 |
50 | $this->adapter->getStorage()->pushConsumedMessageStats(
51 | $event->getEnvelope(),
52 | MessageStatuses::RECEIVED,
53 | $event->getReceiverName()
54 | );
55 | }
56 |
57 | public function onMessageHandled(WorkerMessageHandledEvent $event): void
58 | {
59 | if (!$this->isEnvelopeEnabled($event->getEnvelope())) {
60 | return;
61 | }
62 |
63 | $this->adapter->getStorage()->pushConsumedMessageStats(
64 | $event->getEnvelope(),
65 | MessageStatuses::HANDLED,
66 | $event->getReceiverName()
67 | );
68 | }
69 |
70 | public function onMessageFailed(WorkerMessageFailedEvent $event): void
71 | {
72 | if (!$this->isEnvelopeEnabled($event->getEnvelope())) {
73 | return;
74 | }
75 |
76 | $this->adapter->getStorage()->pushConsumedMessageStats(
77 | $event->getEnvelope(),
78 | MessageStatuses::FAILED,
79 | $event->getReceiverName(),
80 | $event->getThrowable()
81 | );
82 | }
83 |
84 | public function onSyncMessageHandled(SyncMessageHandledEvent $event): void
85 | {
86 | $envelope = $event->getEnvelope();
87 |
88 | if (!$this->isEnvelopeEnabled($envelope)) {
89 | return;
90 | }
91 |
92 | /** @var SentStamp|null $sentStamp */
93 | $sentStamp = $envelope->last(SentStamp::class);
94 | $alias = $sentStamp === null ? 'sync' : ($sentStamp->getSenderAlias() ?: $sentStamp->getSenderClass());
95 |
96 | if ($sentStamp === null) {
97 | $this->adapter->getStorage()->pushSentMessageStats($envelope);
98 | }
99 | $this->adapter->getStorage()->pushConsumedMessageStats($envelope, MessageStatuses::HANDLED, $alias);
100 | }
101 |
102 | public function onSyncMessageFailed(SyncMessageFailedEvent $event): void
103 | {
104 | $envelope = $event->getEnvelope();
105 |
106 | if (!$this->isEnvelopeEnabled($envelope)) {
107 | return;
108 | }
109 |
110 | /** @var SentStamp|null $sentStamp */
111 | $sentStamp = $envelope->last(SentStamp::class);
112 | $alias = $sentStamp === null ? 'sync' : ($sentStamp->getSenderAlias() ?: $sentStamp->getSenderClass());
113 |
114 | if ($sentStamp === null) {
115 | $this->adapter->getStorage()->pushSentMessageStats($envelope);
116 | }
117 | $this->adapter->getStorage()->pushConsumedMessageStats(
118 | $envelope,
119 | MessageStatuses::FAILED,
120 | $alias,
121 | $event->getThrowable()
122 | );
123 | }
124 |
125 | public static function getSubscribedEvents(): array
126 | {
127 | return [
128 | SendMessageToTransportsEvent::class => ['onMessageSent', 99999],
129 | WorkerMessageReceivedEvent::class => ['onMessageReceived', 99999],
130 | WorkerMessageHandledEvent::class => ['onMessageHandled', 99999],
131 | WorkerMessageFailedEvent::class => ['onMessageFailed', 99999],
132 | SyncMessageHandledEvent::class => ['onSyncMessageHandled', 99999],
133 | SyncMessageFailedEvent::class => ['onSyncMessageFailed', 99999],
134 | ];
135 | }
136 |
137 | private function getBusesFromEnvelope(Envelope $envelope): array
138 | {
139 | $busNames = [];
140 |
141 | $stamps = $envelope->all(BusNameStamp::class);
142 | /** @var BusNameStamp $stamp */
143 | foreach ($stamps as $stamp) {
144 | $busNames[] = $stamp->getBusName();
145 | }
146 |
147 | return $busNames;
148 | }
149 |
150 | private function isEnvelopeEnabled(Envelope $envelope): bool
151 | {
152 | if ($envelope->last(IgnoreMonitoringStamp::class)) {
153 | return false;
154 | }
155 |
156 | if (!empty($this->busNames)) {
157 | $busNames = $this->getBusesFromEnvelope($envelope);
158 |
159 | if (count(array_intersect($this->busNames, $busNames)) === 0) {
160 | return false;
161 | }
162 | }
163 |
164 | return true;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/lib/monitoring/messagestats.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class MessageStats
11 | {
12 | /** @var Envelope */
13 | private $envelope;
14 | /** @var string */
15 | private $status;
16 | /** @var string|null */
17 | private $transportName;
18 | /** @var string|null */
19 | private $error;
20 | /** @var \DateTimeInterface */
21 | private $sentAt;
22 | /** @var \DateTimeInterface|null */
23 | private $receivedAt;
24 | /** @var \DateTimeInterface|null */
25 | private $handledAt;
26 | /** @var \DateTimeInterface|null */
27 | private $failedAt;
28 |
29 | public function __construct(
30 | Envelope $envelope,
31 | string $status,
32 | ?string $transportName,
33 | ?string $error,
34 | \DateTimeInterface $sentAt,
35 | ?\DateTimeInterface $receivedAt,
36 | ?\DateTimeInterface $handledAt,
37 | ?\DateTimeInterface $failedAt
38 | ) {
39 | $this->envelope = $envelope;
40 | $this->status = $status;
41 | $this->transportName = $transportName;
42 | $this->error = $error;
43 | $this->sentAt = $sentAt;
44 | $this->receivedAt = $receivedAt;
45 | $this->handledAt = $handledAt;
46 | $this->failedAt = $failedAt;
47 | }
48 |
49 | public function getEnvelope(): Envelope
50 | {
51 | return $this->envelope;
52 | }
53 |
54 | public function getStatus(): string
55 | {
56 | return $this->status;
57 | }
58 |
59 | public function getTransportName(): ?string
60 | {
61 | return $this->transportName;
62 | }
63 |
64 | public function getError(): ?string
65 | {
66 | return $this->error;
67 | }
68 |
69 | public function getSentAt(): \DateTimeInterface
70 | {
71 | return $this->sentAt;
72 | }
73 |
74 | public function getReceivedAt(): ?\DateTimeInterface
75 | {
76 | return $this->receivedAt;
77 | }
78 |
79 | public function getHandledAt(): ?\DateTimeInterface
80 | {
81 | return $this->handledAt;
82 | }
83 |
84 | public function getFailedAt(): ?\DateTimeInterface
85 | {
86 | return $this->failedAt;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/lib/monitoring/messagestatscollection.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class MessageStatsCollection implements \Countable, \IteratorAggregate
9 | {
10 | private $items = [];
11 |
12 | public function add(MessageStats $item): void
13 | {
14 | $this->items[] = $item;
15 | }
16 |
17 | public function toArray(): array
18 | {
19 | return $this->items;
20 | }
21 |
22 | public function getIterator(): \ArrayIterator
23 | {
24 | return new \ArrayIterator($this->items);
25 | }
26 |
27 | public function count(): int
28 | {
29 | return count($this->items);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/monitoring/messagestatuses.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | final class MessageStatuses
9 | {
10 | public const SENT = 'sent';
11 | public const RECEIVED = 'received';
12 | public const HANDLED = 'handled';
13 | public const FAILED = 'failed';
14 | }
15 |
--------------------------------------------------------------------------------
/lib/monitoring/repository/bitrixmessagestatsrepository.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class BitrixMessageStatsRepository implements MessageStatsRepositoryInterface
18 | {
19 | /** @var SerializerInterface */
20 | private $serializer;
21 |
22 | public function __construct(SerializerInterface $serializer = null)
23 | {
24 | $this->serializer = $serializer ?? new PhpSerializer();
25 | }
26 |
27 | /**
28 | * {@inheritDoc}
29 | */
30 | public function countAll(\DateTimeInterface $from, \DateTimeInterface $to, string $search = ''): int
31 | {
32 | $filter = [
33 | '> $this->getDateRange($from, $to),
34 | ];
35 | if (($search = trim($search)) !== '') {
36 | $filter['%MESSAGE'] = $search;
37 | }
38 |
39 | return (int) BitrixMessageStatTable::getCount($filter);
40 | }
41 |
42 | /**
43 | * {@inheritDoc}
44 | */
45 | public function countSent(\DateTimeInterface $from, \DateTimeInterface $to): int
46 | {
47 | return (int) BitrixMessageStatTable::getCount([
48 | '> $this->getDateRange($from, $to),
49 | ]);
50 | }
51 |
52 | /**
53 | * {@inheritDoc}
54 | */
55 | public function countReceived(\DateTimeInterface $from, \DateTimeInterface $to): int
56 | {
57 | return (int) BitrixMessageStatTable::getCount([
58 | '> $this->getDateRange($from, $to),
59 | ]);
60 | }
61 |
62 | /**
63 | * {@inheritDoc}
64 | */
65 | public function countHandled(\DateTimeInterface $from, \DateTimeInterface $to): int
66 | {
67 | return (int) BitrixMessageStatTable::getCount([
68 | '> $this->getDateRange($from, $to),
69 | ]);
70 | }
71 |
72 | /**
73 | * {@inheritDoc}
74 | */
75 | public function countFailed(\DateTimeInterface $from, \DateTimeInterface $to): int
76 | {
77 | return (int) BitrixMessageStatTable::getCount([
78 | '> $this->getDateRange($from, $to),
79 | ]);
80 | }
81 |
82 | /**
83 | * {@inheritDoc}
84 | */
85 | public function getSentChartDataset(\DateTimeInterface $from, \DateTimeInterface $to, int $interval): array
86 | {
87 | return $this->getDatasetByField('SENT_AT', $from->getTimestamp(), $to->getTimestamp(), $interval);
88 | }
89 |
90 | /**
91 | * {@inheritDoc}
92 | */
93 | public function getReceivedChartDataset(\DateTimeInterface $from, \DateTimeInterface $to, int $interval): array
94 | {
95 | return $this->getDatasetByField('RECEIVED_AT', $from->getTimestamp(), $to->getTimestamp(), $interval);
96 | }
97 |
98 | /**
99 | * {@inheritDoc}
100 | */
101 | public function getHandledChartDataset(\DateTimeInterface $from, \DateTimeInterface $to, int $interval): array
102 | {
103 | return $this->getDatasetByField('HANDLED_AT', $from->getTimestamp(), $to->getTimestamp(), $interval);
104 | }
105 |
106 | /**
107 | * {@inheritDoc}
108 | */
109 | public function getFailedChartDataset(\DateTimeInterface $from, \DateTimeInterface $to, int $interval): array
110 | {
111 | return $this->getDatasetByField('FAILED_AT', $from->getTimestamp(), $to->getTimestamp(), $interval);
112 | }
113 |
114 | /**
115 | * {@inheritDoc}
116 | */
117 | public function getRecentList(
118 | \DateTimeInterface $from,
119 | \DateTimeInterface $to,
120 | int $limit,
121 | int $offset,
122 | string $search = ''
123 | ): MessageStatsCollection {
124 | $collection = new MessageStatsCollection();
125 |
126 | $filter = [
127 | [
128 | 'LOGIC' => 'OR',
129 | ['> $this->getDateRange($from, $to)],
130 | ['> $this->getDateRange($from, $to)],
131 | ['> $this->getDateRange($from, $to)],
132 | ['> $this->getDateRange($from, $to)],
133 | ],
134 | ];
135 |
136 | if (($search = trim($search)) !== '') {
137 | $filter['%MESSAGE'] = $search;
138 | }
139 |
140 | $dbResult = BitrixMessageStatTable::getList([
141 | 'select' => ['*'],
142 | 'filter' => $filter,
143 | 'order' => ['SENT_AT' => 'DESC', 'ID' => 'DESC'],
144 | 'limit' => $limit,
145 | 'offset' => $offset,
146 | ]);
147 | while ($row = $dbResult->fetch()) {
148 | $collection->add($this->createMessageStatsFromData($row));
149 | }
150 |
151 | return $collection;
152 | }
153 |
154 | private function getDateRange(\DateTimeInterface $from, \DateTimeInterface $to): array
155 | {
156 | return [
157 | DateTime::createFromTimestamp($from->getTimestamp()),
158 | DateTime::createFromTimestamp($to->getTimestamp()),
159 | ];
160 | }
161 |
162 | private function getDatasetByField(string $field, int $fromTs, int $toTs, int $interval): array
163 | {
164 | $dataset = [];
165 |
166 | $dbResult = BitrixMessageStatTable::getList([
167 | 'select' => ['TIMESTAMP', 'CNT'],
168 | 'filter' => [
169 | "><{$field}" => [
170 | DateTime::createFromTimestamp($fromTs),
171 | DateTime::createFromTimestamp($toTs),
172 | ],
173 | ],
174 | 'group' => ['TIMESTAMP'],
175 | 'runtime' => [
176 | (new ExpressionField('TIMESTAMP', "UNIX_TIMESTAMP(%s) DIV {$interval} * {$interval}", [$field])),
177 | (new ExpressionField('CNT', 'COUNT(1)')),
178 | ],
179 | ]);
180 |
181 | while ($row = $dbResult->fetch()) {
182 | $dataset[] = [(int) $row['TIMESTAMP'], (int) $row['CNT']];
183 | }
184 |
185 | return $dataset;
186 | }
187 |
188 | private function createMessageStatsFromData(array $data): MessageStats
189 | {
190 | $envelope = $this->serializer->decode([
191 | 'body' => $data['BODY'],
192 | 'headers' => $data['HEADERS'] ?? [],
193 | ]);
194 |
195 | return new MessageStats(
196 | $envelope,
197 | $data['STATUS'],
198 | $data['TRANSPORT_NAME'],
199 | $data['ERROR'],
200 | new \DateTime('@' . $data['SENT_AT']->getTimestamp()),
201 | $data['RECEIVED_AT'] instanceof Date ? new \DateTime('@' . $data['SENT_AT']->getTimestamp()) : null,
202 | $data['HANDLED_AT'] instanceof Date ? new \DateTime('@' . $data['HANDLED_AT']->getTimestamp()) : null,
203 | $data['FAILED_AT'] instanceof Date ? new \DateTime('@' . $data['FAILED_AT']->getTimestamp()) : null
204 | );
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/lib/monitoring/repository/messagestatsrepositoryinterface.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | interface MessageStatsRepositoryInterface
11 | {
12 | /**
13 | * Returns a count of all messages.
14 | *
15 | * @param \DateTimeInterface $from
16 | * @param \DateTimeInterface $to
17 | * @param string $search
18 | *
19 | * @return int
20 | */
21 | public function countAll(\DateTimeInterface $from, \DateTimeInterface $to, string $search = ''): int;
22 |
23 | /**
24 | * Returns a count of sent messages.
25 | *
26 | * @param \DateTimeInterface $from
27 | * @param \DateTimeInterface $to
28 | *
29 | * @return int
30 | */
31 | public function countSent(\DateTimeInterface $from, \DateTimeInterface $to): int;
32 |
33 | /**
34 | * Returns a count of received messages.
35 | *
36 | * @param \DateTimeInterface $from
37 | * @param \DateTimeInterface $to
38 | *
39 | * @return int
40 | */
41 | public function countReceived(\DateTimeInterface $from, \DateTimeInterface $to): int;
42 |
43 | /**
44 | * Returns a count of handled messages.
45 | *
46 | * @param \DateTimeInterface $from
47 | * @param \DateTimeInterface $to
48 | *
49 | * @return int
50 | */
51 | public function countHandled(\DateTimeInterface $from, \DateTimeInterface $to): int;
52 |
53 | /**
54 | * Returns a count of failed messages.
55 | *
56 | * @param \DateTimeInterface $from
57 | * @param \DateTimeInterface $to
58 | *
59 | * @return int
60 | */
61 | public function countFailed(\DateTimeInterface $from, \DateTimeInterface $to): int;
62 |
63 | /**
64 | * Returns a chart dataset of sent messages.
65 | *
66 | * @param \DateTimeInterface $from
67 | * @param \DateTimeInterface $to
68 | * @param int $interval
69 | *
70 | * @return array of the format [[$timestamp, $value]]
71 | */
72 | public function getSentChartDataset(\DateTimeInterface $from, \DateTimeInterface $to, int $interval): array;
73 |
74 | /**
75 | * Returns a chart dataset of received messages.
76 | *
77 | * @param \DateTimeInterface $from
78 | * @param \DateTimeInterface $to
79 | * @param int $interval
80 | *
81 | * @return array of the format [[$timestamp, $value]]
82 | */
83 | public function getReceivedChartDataset(\DateTimeInterface $from, \DateTimeInterface $to, int $interval): array;
84 |
85 | /**
86 | * Returns a chart dataset of handled messages.
87 | *
88 | * @param \DateTimeInterface $from
89 | * @param \DateTimeInterface $to
90 | * @param int $interval
91 | *
92 | * @return array of the format [[$timestamp, $value]]
93 | */
94 | public function getHandledChartDataset(\DateTimeInterface $from, \DateTimeInterface $to, int $interval): array;
95 |
96 | /**
97 | * Returns a chart dataset of failed messages.
98 | *
99 | * @param \DateTimeInterface $from
100 | * @param \DateTimeInterface $to
101 | * @param int $interval
102 | *
103 | * @return array of the format [[$timestamp, $value]]
104 | */
105 | public function getFailedChartDataset(\DateTimeInterface $from, \DateTimeInterface $to, int $interval): array;
106 |
107 | /**
108 | * Returns a collection of recent message stats.
109 | *
110 | * @param \DateTimeInterface $from
111 | * @param \DateTimeInterface $to
112 | * @param int $limit
113 | * @param int $offset
114 | * @param string $search
115 | *
116 | * @return MessageStatsCollection
117 | */
118 | public function getRecentList(
119 | \DateTimeInterface $from,
120 | \DateTimeInterface $to,
121 | int $limit,
122 | int $offset,
123 | string $search = ''
124 | ): MessageStatsCollection;
125 | }
126 |
--------------------------------------------------------------------------------
/lib/monitoring/stamp/ignoremonitoringstamp.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class IgnoreMonitoringStamp implements StampInterface
11 | {
12 | }
13 |
--------------------------------------------------------------------------------
/lib/monitoring/storage/bitrixstorage.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | class BitrixStorage implements StorageInterface
21 | {
22 | /** @var SerializerInterface */
23 | private $serializer;
24 |
25 | public function __construct(SerializerInterface $serializer = null)
26 | {
27 | $this->serializer = $serializer ?? new PhpSerializer();
28 | }
29 |
30 | public function pushSentMessageStats(Envelope $envelope): void
31 | {
32 | /** @var UuidStamp|null $uuidStamp */
33 | $uuidStamp = $envelope->last(UuidStamp::class);
34 | if ($uuidStamp === null) {
35 | throw new LogicException('No UuidStamp found on the Envelope.');
36 | }
37 | $uuid = $uuidStamp->getUuid()->toString();
38 |
39 | $encodedMessage = $this->serializer->encode($envelope);
40 |
41 | $result = BitrixMessageStatTable::add([
42 | 'UUID' => $uuid,
43 | 'MESSAGE' => get_class($envelope->getMessage()),
44 | 'STATUS' => MessageStatuses::SENT,
45 | 'BODY' => $encodedMessage['body'],
46 | 'HEADERS' => $encodedMessage['headers'] ?? [],
47 | ]);
48 | if (!$result->isSuccess()) {
49 | throw new RuntimeException(implode("\n", $result->getErrorMessages()));
50 | }
51 | }
52 |
53 | public function pushConsumedMessageStats(
54 | Envelope $envelope,
55 | string $status,
56 | string $transportName,
57 | \Throwable $error = null
58 | ): void {
59 | /** @var UuidStamp|null $uuidStamp */
60 | $uuidStamp = $envelope->last(UuidStamp::class);
61 | if ($uuidStamp === null) {
62 | throw new LogicException('No UuidStamp found on the Envelope.');
63 | }
64 | $uuid = $uuidStamp->getUuid()->toString();
65 |
66 | $row = BitrixMessageStatTable::getRowByUuid($uuid);
67 | if ($row === null) {
68 | throw new RuntimeException(sprintf('Envelope with uuid "%s" not found.', $uuid));
69 | }
70 |
71 | $encodedMessage = $this->serializer->encode($envelope);
72 |
73 | $data = [
74 | 'STATUS' => $status,
75 | 'BODY' => $encodedMessage['body'],
76 | 'HEADERS' => $encodedMessage['headers'] ?? [],
77 | 'TRANSPORT_NAME' => $transportName,
78 | ];
79 |
80 | if ($status === MessageStatuses::RECEIVED) {
81 | $data['RECEIVED_AT'] = new DateTime();
82 | } elseif ($status === MessageStatuses::HANDLED) {
83 | $data['HANDLED_AT'] = new DateTime();
84 | } elseif ($status === MessageStatuses::FAILED) {
85 | $data['FAILED_AT'] = new DateTime();
86 | } else {
87 | throw new InvalidArgumentException(sprintf('The given status is invalid: %s', $status));
88 | }
89 |
90 | if ($error) {
91 | $data['ERROR'] = (string) $error;
92 | }
93 |
94 | $result = BitrixMessageStatTable::update($row['ID'], $data);
95 | if (!$result->isSuccess()) {
96 | throw new RuntimeException(implode("\n", $result->getErrorMessages()));
97 | }
98 | }
99 |
100 | /**
101 | * {@inheritDoc}
102 | */
103 | public function cleanUpStats(int $lifetimeInDays): void
104 | {
105 | $periodInSeconds = $lifetimeInDays * 24 * 3600;
106 | if ($periodInSeconds > 0) {
107 | $connection = Application::getConnection();
108 | $datetime = $connection->getSqlHelper()->addSecondsToDateTime('-' . $periodInSeconds);
109 |
110 | $sql = 'DELETE FROM `' . BitrixMessageStatTable::getTableName() . '` WHERE `SENT_AT` <= ' . $datetime;
111 | $connection->query($sql);
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/lib/monitoring/storage/storageinterface.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | interface StorageInterface
11 | {
12 | /**
13 | * Pushes a sent message stats.
14 | *
15 | * @param Envelope $envelope
16 | */
17 | public function pushSentMessageStats(Envelope $envelope): void;
18 |
19 | /**
20 | * Pushes a consumed message stats.
21 | *
22 | * @param Envelope $envelope
23 | * @param string $status
24 | * @param string $transportName
25 | * @param \Throwable|null $error
26 | */
27 | public function pushConsumedMessageStats(
28 | Envelope $envelope,
29 | string $status,
30 | string $transportName,
31 | \Throwable $error = null
32 | ): void;
33 |
34 | /**
35 | * Removes old stats.
36 | *
37 | * @param int $lifetimeInDays
38 | */
39 | public function cleanUpStats(int $lifetimeInDays): void;
40 | }
41 |
--------------------------------------------------------------------------------
/lib/queue.php:
--------------------------------------------------------------------------------
1 |
40 | */
41 | class Queue
42 | {
43 | /** @var Container */
44 | protected $container;
45 | /** @var array */
46 | protected $config;
47 | /** @var bool */
48 | protected $booted = false;
49 | /** @var bool */
50 | protected $useCache = false;
51 |
52 | /** @var Queue */
53 | private static $instance;
54 |
55 | protected const CONFIG_KEY = 'bsi.queue';
56 | protected const DEFAULT_CONFIG = [
57 | 'buses' => [
58 | 'default' => [
59 | 'default_middleware' => true,
60 | 'middleware' => [],
61 | ],
62 | ],
63 | 'default_bus' => null,
64 | 'transports' => [],
65 | 'failure_transport' => null,
66 | 'routing' => [],
67 | 'monitoring' => [],
68 | ];
69 | protected const DEFAULT_BUS_CONFIG = [
70 | 'default_middleware' => true,
71 | 'middleware' => [],
72 | ];
73 | protected const DEFAULT_TRANSPORT_CONFIG = [
74 | 'options' => [],
75 | 'failure_transport' => null,
76 | 'serializer' => null,
77 | 'retry_strategy' => [
78 | 'max_retries' => 3,
79 | 'multiplier' => 2,
80 | 'service' => null,
81 | 'delay' => 1000,
82 | 'max_delay' => 0,
83 | ],
84 | ];
85 | protected const DEFAULT_MONITORING_CONFIG = [
86 | 'enabled' => true,
87 | 'adapter' => 'bitrix',
88 | 'buses' => [],
89 | ];
90 |
91 | public static function getInstance(): self
92 | {
93 | if (self::$instance === null) {
94 | self::$instance = new self();
95 | }
96 |
97 | return self::$instance;
98 | }
99 |
100 | private function __construct()
101 | {
102 | $config = (array) Configuration::getValue(static::CONFIG_KEY);
103 |
104 | $event = new Event('bsi.queue', QueueEvents::LOAD_CONFIGURATION, $config);
105 | $event->send();
106 | $resultList = $event->getResults();
107 | foreach ($resultList as $eventResult) {
108 | if ($eventResult->getType() !== EventResult::SUCCESS) {
109 | continue;
110 | }
111 |
112 | $params = $eventResult->getParameters();
113 | if (!empty($params) && is_array($params)) {
114 | foreach ($params as $key => $value) {
115 | $config[$key] = $value;
116 | }
117 | }
118 | }
119 |
120 | $config = $this->normalizeConfiguration($config);
121 |
122 | if ($config['default_bus'] === null && count($config['buses']) === 1) {
123 | $config['default_bus'] = key($config['buses']);
124 | }
125 |
126 | $this->config = $config;
127 | $this->container = new ContainerBuilder();
128 | }
129 |
130 | public function useCache(bool $useCache): void
131 | {
132 | $this->useCache = $useCache;
133 | }
134 |
135 | public function boot(string $cacheFile = '/bitrix/cache/bsi_queue_container.php'): void
136 | {
137 | if ($this->booted === true) {
138 | return;
139 | }
140 |
141 | if ($this->useCache) {
142 | $cacheFileFull = $_SERVER['DOCUMENT_ROOT'] . $cacheFile;
143 | $containerConfigCache = new ConfigCache($cacheFileFull, false);
144 |
145 | if (!$containerConfigCache->isFresh()) {
146 | $this->initializeContainer();
147 |
148 | $dumper = new PhpDumper($this->container);
149 | $containerConfigCache->write(
150 | $dumper->dump(['class' => 'BsiQueueCachedContainer']),
151 | $this->container->getResources()
152 | );
153 | }
154 |
155 | require_once $cacheFileFull;
156 | /** @noinspection PhpUndefinedClassInspection */
157 | $this->container = new \BsiQueueCachedContainer();
158 | } else {
159 | $this->initializeContainer();
160 | }
161 |
162 | $this->booted = true;
163 | }
164 |
165 | public function addMessageHandler(string $class, array $arguments = [], array $options = []): void
166 | {
167 | if (!is_subclass_of($class, MessageHandlerInterface::class)) {
168 | throw new RuntimeException(sprintf('Class "%s" must implement interface "%s".', $class, MessageHandlerInterface::class));
169 | }
170 |
171 | $service = $this->container->register($class, $class);
172 | foreach ($arguments as $argument) {
173 | $service->addArgument($argument);
174 | }
175 | $service->addTag('messenger.message_handler', $options);
176 | }
177 |
178 | public function registerTransportFactory(string $code, string $class, array $arguments = []): void
179 | {
180 | if (!is_subclass_of($class, TransportFactoryInterface::class)) {
181 | throw new RuntimeException(sprintf('Class "%s" must implement interface "%s".', $class, TransportFactoryInterface::class));
182 | }
183 |
184 | $service = $this->container->register('messenger.transport.' . $code . '.factory', $class);
185 | foreach ($arguments as $argument) {
186 | $service->addArgument($argument);
187 | }
188 | $service->addTag('messenger.transport_factory');
189 | }
190 |
191 | public function registerMonitoringAdapterFactory(string $code, string $class, array $arguments = []): void
192 | {
193 | if (!is_subclass_of($class, AdapterFactoryInterface::class)) {
194 | throw new RuntimeException(sprintf('Class "%s" must implement interface "%s".', $class, AdapterFactoryInterface::class));
195 | }
196 |
197 | $service = $this->container->register('monitoring.adapter.' . $code . '.factory', $class);
198 | foreach ($arguments as $argument) {
199 | $service->addArgument($argument);
200 | }
201 | $service->addTag('monitoring.adapter_factory');
202 | }
203 |
204 | public function dispatchMessage(object $message, ?string $busName = null, array $stamps = []): Envelope
205 | {
206 | if ($this->booted === false) {
207 | throw new RuntimeException('Dispatching the message from a non-booted Queue is forbidden.');
208 | }
209 |
210 | $busName = $busName ?? $this->config['default_bus'];
211 | /** @var MessageBusInterface|null $bus */
212 | $bus = $this->container->get($busName);
213 | /** @var EventDispatcherInterface $eventDispatcher */
214 | $eventDispatcher = $this->container->get('event_dispatcher');
215 |
216 | if ($bus === null) {
217 | throw new RuntimeException(sprintf('Bus "%s" does not exist.', $busName));
218 | }
219 |
220 | try {
221 | $envelope = $bus->dispatch(Envelope::wrap($message, $stamps), $stamps);
222 |
223 | /** @var HandledStamp|null $handledStamp */
224 | $handledStamp = $envelope->last(HandledStamp::class);
225 | if ($handledStamp) {
226 | $eventDispatcher->dispatch(new SyncMessageHandledEvent($envelope));
227 | }
228 | } catch (HandlerFailedException $e) {
229 | $eventDispatcher->dispatch(new SyncMessageFailedEvent($e->getEnvelope(), $e));
230 | throw $e;
231 | }
232 |
233 | return $envelope;
234 | }
235 |
236 | public function getContainer(): Container
237 | {
238 | return $this->container;
239 | }
240 |
241 | public function getConfig(): array
242 | {
243 | return $this->config;
244 | }
245 |
246 | protected function normalizeConfiguration(array $config): array
247 | {
248 | $config = array_merge(static::DEFAULT_CONFIG, $config);
249 |
250 | $newBusesConfig = [];
251 | foreach ($config['buses'] as $id => $bus) {
252 | if (is_int($id)) {
253 | $id = $bus;
254 | $bus = [];
255 | }
256 | $newBusesConfig[$id] = array_replace_recursive(static::DEFAULT_BUS_CONFIG, (array) $bus);
257 | }
258 | $config['buses'] = $newBusesConfig;
259 |
260 | $newTransportsConfig = [];
261 | foreach ($config['transports'] as $id => $transport) {
262 | if (is_string($transport)) {
263 | $transport = [
264 | 'dsn' => $transport,
265 | ];
266 | }
267 | $newTransportsConfig[$id] = array_replace_recursive(static::DEFAULT_TRANSPORT_CONFIG, (array) $transport);
268 | }
269 | $config['transports'] = $newTransportsConfig;
270 |
271 | $newRoutingConfig = [];
272 | foreach ($config['routing'] as $message => $transports) {
273 | $newRoutingConfig[$message] = ['senders' => (array) $transports];
274 | }
275 | $config['routing'] = $newRoutingConfig;
276 |
277 | return $config;
278 | }
279 |
280 | protected function initializeContainer(): void
281 | {
282 | $this->container->addObjectResource($this);
283 | $this->container->addCompilerPass(new RegisterListenersPass());
284 | $this->container->addCompilerPass(new MessengerPass());
285 | $this->container->addCompilerPass(new AddConsoleCommandPass());
286 |
287 | $this->container->register('event_dispatcher', EventDispatcher::class)->setPublic(true);
288 |
289 | $loader = new XmlFileLoader($this->container, new FileLocator(dirname(__DIR__) . '/config'));
290 | $loader->load('messenger.xml');
291 | $loader->load('monitoring.xml');
292 |
293 | $this->registerMessengerConfiguration($this->config, $this->container);
294 | $this->registerMonitoringConfiguration($this->config, $this->container);
295 |
296 | $this->container->compile();
297 | }
298 |
299 | private function registerMessengerConfiguration(array $config, ContainerBuilder $container): void
300 | {
301 | if ($config['default_bus'] === null && count($config['buses']) === 1) {
302 | $config['default_bus'] = key($config['buses']);
303 | }
304 |
305 | $defaultMiddleware = [
306 | 'before' => [
307 | ['id' => 'add_bus_name_stamp_middleware'],
308 | ['id' => 'add_uuid_stamp_middleware'],
309 | ['id' => 'reject_redelivered_message_middleware'],
310 | ['id' => 'dispatch_after_current_bus'],
311 | ['id' => 'failed_message_processing_middleware'],
312 | ],
313 | 'after' => [
314 | ['id' => 'send_message'],
315 | ['id' => 'handle_message'],
316 | ],
317 | ];
318 | foreach ($config['buses'] as $busId => $bus) {
319 | $middleware = $bus['middleware'];
320 |
321 | if ($bus['default_middleware']) {
322 | if ($bus['default_middleware'] === 'allow_no_handlers') {
323 | $defaultMiddleware['after'][1]['arguments'] = [true];
324 | } else {
325 | unset($defaultMiddleware['after'][1]['arguments']);
326 | }
327 |
328 | // argument to add_bus_name_stamp_middleware
329 | $defaultMiddleware['before'][0]['arguments'] = [$busId];
330 |
331 | $middleware = array_merge($defaultMiddleware['before'], $middleware, $defaultMiddleware['after']);
332 | }
333 |
334 | $container->setParameter($busId . '.middleware', $middleware);
335 | $container->register($busId, MessageBus::class)
336 | ->addArgument([])
337 | ->setPublic(true)
338 | ->addTag('messenger.bus');
339 |
340 | if ($config['default_bus'] === $busId) {
341 | $container->setAlias('messenger.default_bus', $busId)->setPublic(true);
342 | $container->setAlias(MessageBusInterface::class, $busId);
343 | } else {
344 | $container->registerAliasForArgument($busId, MessageBusInterface::class);
345 | }
346 | }
347 |
348 | if (empty($config['transports'])) {
349 | $container->removeDefinition('messenger.transport.symfony_serializer');
350 | $container->removeDefinition('messenger.transport.amqp.factory');
351 | $container->removeDefinition('messenger.transport.redis.factory');
352 | $container->removeDefinition('messenger.transport.sqs.factory');
353 | $container->removeDefinition('messenger.transport.beanstalkd.factory');
354 | }
355 |
356 | $failureTransports = [];
357 | if ($config['failure_transport']) {
358 | if (!isset($config['transports'][$config['failure_transport']])) {
359 | throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $config['failure_transport']));
360 | }
361 |
362 | $container->setAlias('messenger.failure_transports.default', 'messenger.transport.' . $config['failure_transport']);
363 | $failureTransports[] = $config['failure_transport'];
364 | }
365 |
366 | $failureTransportsByName = [];
367 | foreach ($config['transports'] as $name => $transport) {
368 | if ($transport['failure_transport']) {
369 | $failureTransports[] = $transport['failure_transport'];
370 | $failureTransportsByName[$name] = $transport['failure_transport'];
371 | } elseif ($config['failure_transport']) {
372 | $failureTransportsByName[$name] = $config['failure_transport'];
373 | }
374 | }
375 |
376 | $senderAliases = [];
377 | $transportRetryReferences = [];
378 | foreach ($config['transports'] as $name => $transport) {
379 | $serializerId = $transport['serializer'] ?? 'messenger.default_serializer';
380 | $transportDefinition = (new Definition(TransportInterface::class))
381 | ->setFactory([new Reference('messenger.transport_factory'), 'createTransport'])
382 | ->setArguments([$transport['dsn'], $transport['options'] + ['transport_name' => $name], new Reference($serializerId)])
383 | ->addTag('messenger.receiver', [
384 | 'alias' => $name,
385 | 'is_failure_transport' => in_array($name, $failureTransports, true),
386 | ]);
387 | $container->setDefinition($transportId = 'messenger.transport.' . $name, $transportDefinition);
388 | $senderAliases[$name] = $transportId;
389 |
390 | if ($transport['retry_strategy']['service'] !== null) {
391 | $transportRetryReferences[$name] = new Reference($transport['retry_strategy']['service']);
392 | } else {
393 | $retryServiceId = sprintf('messenger.retry.multiplier_retry_strategy.%s', $name);
394 | $retryDefinition = new ChildDefinition('messenger.retry.abstract_multiplier_retry_strategy');
395 | $retryDefinition
396 | ->replaceArgument(0, $transport['retry_strategy']['max_retries'])
397 | ->replaceArgument(1, $transport['retry_strategy']['delay'])
398 | ->replaceArgument(2, $transport['retry_strategy']['multiplier'])
399 | ->replaceArgument(3, $transport['retry_strategy']['max_delay']);
400 | $container->setDefinition($retryServiceId, $retryDefinition);
401 |
402 | $transportRetryReferences[$name] = new Reference($retryServiceId);
403 | }
404 | }
405 |
406 | $senderReferences = [];
407 | // alias => service_id
408 | foreach ($senderAliases as $alias => $serviceId) {
409 | $senderReferences[$alias] = new Reference($serviceId);
410 | }
411 | // service_id => service_id
412 | foreach ($senderAliases as $serviceId) {
413 | $senderReferences[$serviceId] = new Reference($serviceId);
414 | }
415 |
416 | foreach ($config['transports'] as $transport) {
417 | if ($transport['failure_transport'] && !isset($senderReferences[$transport['failure_transport']])) {
418 | throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $transport['failure_transport']));
419 | }
420 | }
421 |
422 | $failureTransportReferencesByTransportName = array_map(static function ($failureTransportName) use ($senderReferences) {
423 | return $senderReferences[$failureTransportName];
424 | }, $failureTransportsByName);
425 |
426 | $messageToSendersMapping = [];
427 | foreach ($config['routing'] as $message => $messageConfiguration) {
428 | if ($message !== '*' && !class_exists($message) && !interface_exists($message, false)) {
429 | throw new LogicException(sprintf('Invalid Messenger routing configuration: class or interface "%s" not found.', $message));
430 | }
431 |
432 | // make sure senderAliases contains all senders
433 | foreach ($messageConfiguration['senders'] as $sender) {
434 | if (!isset($senderReferences[$sender])) {
435 | throw new LogicException(sprintf('Invalid Messenger routing configuration: the "%s" class is being routed to a sender called "%s". This is not a valid transport or service id.', $message, $sender));
436 | }
437 | }
438 |
439 | $messageToSendersMapping[$message] = $messageConfiguration['senders'];
440 | }
441 |
442 | $sendersServiceLocator = ServiceLocatorTagPass::register($container, $senderReferences);
443 |
444 | $container->getDefinition('messenger.senders_locator')
445 | ->replaceArgument(0, $messageToSendersMapping)
446 | ->replaceArgument(1, $sendersServiceLocator)
447 | ;
448 |
449 | $container->getDefinition('messenger.retry.send_failed_message_for_retry_listener')
450 | ->replaceArgument(0, $sendersServiceLocator)
451 | ;
452 |
453 | $container->getDefinition('messenger.retry_strategy_locator')
454 | ->replaceArgument(0, $transportRetryReferences);
455 |
456 | if ($failureTransports) {
457 | $container->getDefinition('console.command.messenger_failed_messages_retry')
458 | ->replaceArgument(0, $config['failure_transport']);
459 | $container->getDefinition('console.command.messenger_failed_messages_show')
460 | ->replaceArgument(0, $config['failure_transport']);
461 | $container->getDefinition('console.command.messenger_failed_messages_remove')
462 | ->replaceArgument(0, $config['failure_transport']);
463 |
464 | $failureTransportsByTransportNameServiceLocator = ServiceLocatorTagPass::register($container, $failureTransportReferencesByTransportName);
465 | $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')
466 | ->replaceArgument(0, $failureTransportsByTransportNameServiceLocator);
467 | } else {
468 | $container->removeDefinition('messenger.failure.send_failed_message_to_failure_transport_listener');
469 | $container->removeDefinition('console.command.messenger_failed_messages_retry');
470 | $container->removeDefinition('console.command.messenger_failed_messages_show');
471 | $container->removeDefinition('console.command.messenger_failed_messages_remove');
472 | }
473 | }
474 |
475 | private function registerMonitoringConfiguration(array $config, Container $container): void
476 | {
477 | $monitoringConfig = array_replace_recursive(static::DEFAULT_MONITORING_CONFIG, $config['monitoring']);
478 |
479 | $busNames = $monitoringConfig['buses'] ?? [];
480 | $allowedBuses = array_keys($config['buses']);
481 |
482 | if (count($busNames) !== count(array_intersect($busNames, $allowedBuses))) {
483 | throw new RuntimeException(sprintf('Unknown bus found: [%s]. Allowed buses are [%s].', implode(', ', $busNames), implode(', ', $allowedBuses)));
484 | }
485 |
486 | $adapterDefinition = (new Definition(AdapterInterface::class))
487 | ->setFactory([new Reference('monitoring.adapter_factory'), 'createAdapter'])
488 | ->setArguments([
489 | $monitoringConfig['adapter'],
490 | $monitoringConfig['options'] ?? [],
491 | ]);
492 |
493 | $container->setDefinition('monitoring.adapter', $adapterDefinition);
494 | $container->setAlias(AdapterInterface::class, 'monitoring.adapter')->setPublic(true);
495 |
496 | $container->getDefinition('monitoring.push_stats_listener')
497 | ->replaceArgument(0, new Reference('monitoring.adapter'))
498 | ->replaceArgument(1, array_values($busNames));
499 |
500 | $isEnabled = $monitoringConfig['enabled'] ?? false;
501 | if (!$isEnabled) {
502 | $container->removeDefinition('monitoring.push_stats_listener');
503 | }
504 | }
505 | }
506 |
--------------------------------------------------------------------------------
/lib/queueevents.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | final class QueueEvents
9 | {
10 | public const LOAD_CONFIGURATION = 'onLoadConfiguration';
11 | }
12 |
--------------------------------------------------------------------------------
/lib/stamp/uuidstamp.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class UuidStamp implements StampInterface
13 | {
14 | /** @var UuidInterface */
15 | private $uuid;
16 |
17 | public function __construct()
18 | {
19 | $this->uuid = Uuid::uuid4();
20 | }
21 |
22 | public function getUuid(): UuidInterface
23 | {
24 | return $this->uuid;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/transport/bitrix/bitrixreceivedstamp.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class BitrixReceivedStamp implements NonSendableStampInterface
11 | {
12 | private $id;
13 |
14 | public function __construct(int $id)
15 | {
16 | $this->id = $id;
17 | }
18 |
19 | public function getId(): int
20 | {
21 | return $this->id;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/transport/bitrix/bitrixreceiver.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | class BitrixReceiver implements ReceiverInterface, MessageCountAwareInterface, ListableReceiverInterface
20 | {
21 | private const MAX_RETRIES = 3;
22 | /** @var int */
23 | private $retryingSafetyCounter = 0;
24 | /** @var Connection */
25 | private $connection;
26 | /** @var SerializerInterface */
27 | private $serializer;
28 |
29 | public function __construct(Connection $connection, SerializerInterface $serializer = null)
30 | {
31 | $this->connection = $connection;
32 | $this->serializer = $serializer ?? new PhpSerializer();
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function get(): iterable
39 | {
40 | try {
41 | $bitrixEnvelope = $this->connection->get();
42 | $this->retryingSafetyCounter = 0;
43 | } catch (TransportException $exception) {
44 | if (++$this->retryingSafetyCounter >= self::MAX_RETRIES) {
45 | $this->retryingSafetyCounter = 0;
46 | throw new TransportException($exception->getMessage(), 0, $exception);
47 | }
48 |
49 | return [];
50 | }
51 |
52 | if ($bitrixEnvelope === null) {
53 | return [];
54 | }
55 |
56 | return [$this->createEnvelopeFromData($bitrixEnvelope)];
57 | }
58 |
59 | /**
60 | * {@inheritdoc}
61 | */
62 | public function ack(Envelope $envelope): void
63 | {
64 | $this->connection->ack($this->findBitrixReceivedStamp($envelope)->getId());
65 | }
66 |
67 | /**
68 | * {@inheritdoc}
69 | */
70 | public function reject(Envelope $envelope): void
71 | {
72 | $this->connection->reject($this->findBitrixReceivedStamp($envelope)->getId());
73 | }
74 |
75 | /**
76 | * {@inheritdoc}
77 | */
78 | public function getMessageCount(): int
79 | {
80 | return $this->connection->getMessageCount();
81 | }
82 |
83 | /**
84 | * {@inheritdoc}
85 | */
86 | public function all(int $limit = null): iterable
87 | {
88 | $bitrixEnvelopes = $this->connection->findAll($limit);
89 |
90 | foreach ($bitrixEnvelopes as $bitrixEnvelope) {
91 | yield $this->createEnvelopeFromData($bitrixEnvelope);
92 | }
93 | }
94 |
95 | /**
96 | * {@inheritdoc}
97 | */
98 | public function find($id): ?Envelope
99 | {
100 | $bitrixEnvelope = $this->connection->find($id);
101 |
102 | if ($bitrixEnvelope === null) {
103 | return null;
104 | }
105 |
106 | return $this->createEnvelopeFromData($bitrixEnvelope);
107 | }
108 |
109 | private function findBitrixReceivedStamp(Envelope $envelope): BitrixReceivedStamp
110 | {
111 | /** @var BitrixReceivedStamp|null $bitrixReceivedStamp */
112 | $bitrixReceivedStamp = $envelope->last(BitrixReceivedStamp::class);
113 |
114 | if ($bitrixReceivedStamp === null) {
115 | throw new LogicException('No BitrixReceivedStamp found on the Envelope.');
116 | }
117 |
118 | return $bitrixReceivedStamp;
119 | }
120 |
121 | private function createEnvelopeFromData(array $data): Envelope
122 | {
123 | try {
124 | $envelope = $this->serializer->decode([
125 | 'body' => $data['BODY'],
126 | 'headers' => $data['HEADERS'] ?? [],
127 | ]);
128 | } catch (MessageDecodingFailedException $exception) {
129 | $this->connection->reject((int) $data['ID']);
130 |
131 | throw $exception;
132 | }
133 |
134 | return $envelope->with(
135 | new BitrixReceivedStamp((int) $data['ID']),
136 | new TransportMessageIdStamp((int) $data['ID'])
137 | );
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/lib/transport/bitrix/bitrixsender.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class BitrixSender implements SenderInterface
16 | {
17 | /** @var Connection */
18 | private $connection;
19 | /** @var SerializerInterface */
20 | private $serializer;
21 |
22 | public function __construct(Connection $connection, SerializerInterface $serializer = null)
23 | {
24 | $this->connection = $connection;
25 | $this->serializer = $serializer ?? new PhpSerializer();
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function send(Envelope $envelope): Envelope
32 | {
33 | $encodedMessage = $this->serializer->encode($envelope);
34 |
35 | /** @var DelayStamp|null $delayStamp */
36 | $delayStamp = $envelope->last(DelayStamp::class);
37 | $delay = null !== $delayStamp ? $delayStamp->getDelay() : 0;
38 |
39 | $id = $this->connection->send($encodedMessage['body'], $encodedMessage['headers'] ?? [], $delay);
40 |
41 | return $envelope->with(new TransportMessageIdStamp($id));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/transport/bitrix/bitrixtransport.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class BitrixTransport implements TransportInterface, MessageCountAwareInterface, ListableReceiverInterface
15 | {
16 | /** @var Connection */
17 | private $connection;
18 | /** @var SerializerInterface */
19 | private $serializer;
20 | /** @var BitrixReceiver */
21 | private $receiver;
22 | /** @var BitrixSender */
23 | private $sender;
24 |
25 | public function __construct(Connection $connection, SerializerInterface $serializer)
26 | {
27 | $this->connection = $connection;
28 | $this->serializer = $serializer;
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function get(): iterable
35 | {
36 | return ($this->receiver ?? $this->getReceiver())->get();
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | */
42 | public function ack(Envelope $envelope): void
43 | {
44 | ($this->receiver ?? $this->getReceiver())->ack($envelope);
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | public function reject(Envelope $envelope): void
51 | {
52 | ($this->receiver ?? $this->getReceiver())->reject($envelope);
53 | }
54 |
55 | /**
56 | * {@inheritdoc}
57 | */
58 | public function getMessageCount(): int
59 | {
60 | return ($this->receiver ?? $this->getReceiver())->getMessageCount();
61 | }
62 |
63 | /**
64 | * {@inheritdoc}
65 | */
66 | public function all(int $limit = null): iterable
67 | {
68 | return ($this->receiver ?? $this->getReceiver())->all($limit);
69 | }
70 |
71 | /**
72 | * {@inheritdoc}
73 | */
74 | public function find($id): ?Envelope
75 | {
76 | return ($this->receiver ?? $this->getReceiver())->find($id);
77 | }
78 |
79 | /**
80 | * {@inheritdoc}
81 | */
82 | public function send(Envelope $envelope): Envelope
83 | {
84 | return ($this->sender ?? $this->getSender())->send($envelope);
85 | }
86 |
87 | private function getReceiver(): BitrixReceiver
88 | {
89 | return $this->receiver = new BitrixReceiver($this->connection, $this->serializer);
90 | }
91 |
92 | private function getSender(): BitrixSender
93 | {
94 | return $this->sender = new BitrixSender($this->connection, $this->serializer);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/lib/transport/bitrix/bitrixtransportfactory.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class BitrixTransportFactory implements TransportFactoryInterface
13 | {
14 | public function createTransport(string $dsn, array $options, SerializerInterface $serializer): TransportInterface
15 | {
16 | unset($options['transport_name']);
17 |
18 | if (preg_match('/:\/\/$/', $dsn)) {
19 | $configuration = $options;
20 | } else {
21 | $configuration = Connection::buildConfiguration($dsn, $options);
22 | }
23 |
24 | return new BitrixTransport(new Connection($configuration), $serializer);
25 | }
26 |
27 | public function supports(string $dsn, array $options): bool
28 | {
29 | return strpos($dsn, 'bitrix://') === 0;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/transport/bitrix/connection.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class Connection
17 | {
18 | protected const DEFAULT_OPTIONS = [
19 | 'queue_name' => 'default',
20 | 'redeliver_timeout' => 3600,
21 | ];
22 |
23 | /**
24 | * Configuration of the connection.
25 | *
26 | * Available options:
27 | *
28 | * * queue_name: name of the queue
29 | * * redeliver_timeout: Timeout before redeliver messages still in handling state (i.e: delivered_at is not null and message is still in table). Default: 3600
30 | */
31 | protected $configuration = [];
32 |
33 | private $doMysqlCleanup;
34 |
35 | public function __construct(array $configuration = [])
36 | {
37 | $this->configuration = array_replace_recursive(static::DEFAULT_OPTIONS, $configuration);
38 | $this->doMysqlCleanup = false;
39 | }
40 |
41 | public static function buildConfiguration(string $dsn, array $options = []): array
42 | {
43 | $query = [];
44 | if ($queryAsString = strstr($dsn, '?')) {
45 | parse_str(ltrim($queryAsString, '?'), $query);
46 | }
47 |
48 | $configuration = [];
49 | /** @noinspection AdditionOperationOnArraysInspection */
50 | $configuration += $query + $options + static::DEFAULT_OPTIONS;
51 |
52 | $optionsExtraKeys = array_diff(array_keys($options), array_keys(static::DEFAULT_OPTIONS));
53 | if (count($optionsExtraKeys) > 0) {
54 | throw new InvalidArgumentException(sprintf('Unknown option found: [%s]. Allowed options are [%s].', implode(', ', $optionsExtraKeys), implode(', ', array_keys(static::DEFAULT_OPTIONS))));
55 | }
56 |
57 | $queryExtraKeys = array_diff(array_keys($query), array_keys(static::DEFAULT_OPTIONS));
58 | if (count($queryExtraKeys) > 0) {
59 | throw new InvalidArgumentException(sprintf('Unknown option found in DSN: [%s]. Allowed options are [%s].', implode(', ', $queryExtraKeys), implode(', ', array_keys(static::DEFAULT_OPTIONS))));
60 | }
61 |
62 | return $configuration;
63 | }
64 |
65 | public function getConfiguration(): array
66 | {
67 | return $this->configuration;
68 | }
69 |
70 | public function send(string $body, array $headers, int $delay = 0): int
71 | {
72 | $now = new DateTime();
73 | $availableAt = (clone $now)->add(sprintf('+%d seconds', $delay / 1000));
74 |
75 | $result = MessageTable::add([
76 | 'BODY' => $body,
77 | 'HEADERS' => $headers,
78 | 'QUEUE_NAME' => $this->configuration['queue_name'],
79 | 'CREATED_AT' => $now,
80 | 'AVAILABLE_AT' => $availableAt,
81 | ]);
82 | if (!$result->isSuccess()) {
83 | throw new TransportException(implode("\n", $result->getErrorMessages()), 0);
84 | }
85 |
86 | return (int) $result->getId();
87 | }
88 |
89 | public function get(): ?array
90 | {
91 | $driverConnection = Application::getConnection(MessageTable::getConnectionName());
92 |
93 | if ($this->doMysqlCleanup && $this->isMysqlConnection()) {
94 | try {
95 | $driverConnection->query(sprintf(
96 | "DELETE FROM %s WHERE DELIVERED_AT = '9999-12-31 23:59:59'",
97 | MessageTable::getTableName(),
98 | ));
99 | $this->doMysqlCleanup = false;
100 | } catch (SqlQueryException $e) {
101 | // Ignore the exception
102 | }
103 | }
104 |
105 | $driverConnection->startTransaction();
106 | try {
107 | $query = $this->createAvailableMessagesQuery()
108 | ->addOrder('AVAILABLE_AT', 'ASC')
109 | ->setLimit(1);
110 |
111 | $bitrixEnvelope = $query->exec()->fetch();
112 |
113 | if ($bitrixEnvelope === false || $bitrixEnvelope === null) {
114 | $driverConnection->commitTransaction();
115 | return null;
116 | }
117 |
118 | $result = MessageTable::update($bitrixEnvelope['ID'], ['DELIVERED_AT' => new DateTime()]);
119 | if (!$result->isSuccess()) {
120 | throw new TransportException(implode("\n", $result->getErrorMessages()), 0);
121 | }
122 |
123 | $driverConnection->commitTransaction();
124 |
125 | return $bitrixEnvelope;
126 | } catch (\Throwable $e) {
127 | $driverConnection->rollbackTransaction();
128 | throw $e;
129 | }
130 | }
131 |
132 | public function ack(int $id): bool
133 | {
134 | if ($this->isMysqlConnection()) {
135 | $result = MessageTable::update($id, ['DELIVERED_AT' => new DateTime('9999-12-31 23:59:59', 'Y-m-d H:i:s')]);
136 | $updated = $result->isSuccess();
137 |
138 | if ($updated) {
139 | $this->doMysqlCleanup = true;
140 | }
141 |
142 | return $updated;
143 | }
144 |
145 | $result = MessageTable::delete($id);
146 | if (!$result->isSuccess()) {
147 | throw new TransportException(implode("\n", $result->getErrorMessages()), 0);
148 | }
149 |
150 | return true;
151 | }
152 |
153 | public function reject(int $id): bool
154 | {
155 | return $this->ack($id);
156 | }
157 |
158 | public function getMessageCount(): int
159 | {
160 | $query = $this->createAvailableMessagesQuery()
161 | ->setSelect(['CNT' => Query::expr()->count('ID')])
162 | ->setLimit(1);
163 |
164 | $data = $query->exec()->fetch();
165 |
166 | return (int) $data['CNT'];
167 | }
168 |
169 | public function findAll(int $limit = null): array
170 | {
171 | $query = $this->createAvailableMessagesQuery();
172 | if ($limit !== null) {
173 | $query->setLimit($limit);
174 | }
175 |
176 | return $query->exec()->fetchAll();
177 | }
178 |
179 | public function find(int $id): ?array
180 | {
181 | return MessageTable::getRowById($id);
182 | }
183 |
184 | private function createAvailableMessagesQuery(): Query
185 | {
186 | $now = new DateTime();
187 | $redeliverLimit = (clone $now)->add(sprintf('-%d seconds', $this->configuration['redeliver_timeout']));
188 |
189 | return MessageTable::query()
190 | ->setSelect(['*'])
191 | ->where(
192 | Query::filter()->logic('OR')
193 | ->whereNull('DELIVERED_AT')
194 | ->where('DELIVERED_AT', '<', $redeliverLimit)
195 | )
196 | ->where('AVAILABLE_AT', '<=', $now)
197 | ->where('QUEUE_NAME', $this->configuration['queue_name']);
198 | }
199 |
200 | private function isMysqlConnection(): bool
201 | {
202 | return Application::getConnection(MessageTable::getConnectionName()) instanceof MysqlCommonConnection;
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/lib/transport/bitrix/messagetable.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class MessageTable extends DataManager
16 | {
17 | public static function getTableName(): string
18 | {
19 | return 'bsi_queue_message';
20 | }
21 |
22 | public static function getMap(): array
23 | {
24 | return [
25 | (new IntegerField('ID'))
26 | ->configurePrimary(true)
27 | ->configureAutocomplete(true),
28 |
29 | (new TextField('BODY'))
30 | ->configureRequired(true),
31 |
32 | (new ArrayField('HEADERS'))
33 | ->configureSerializationPhp(),
34 |
35 | (new StringField('QUEUE_NAME'))
36 | ->configureRequired(true)
37 | ->configureSize(190),
38 |
39 | (new DatetimeField('CREATED_AT'))
40 | ->configureRequired(true),
41 |
42 | (new DatetimeField('AVAILABLE_AT'))
43 | ->configureRequired(true),
44 |
45 | (new DatetimeField('DELIVERED_AT')),
46 | ];
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/utils/chartintervalcalculator.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class ChartIntervalCalculator
9 | {
10 | /** @var int */
11 | private $resolution;
12 |
13 | public function __construct(int $resolution = 100)
14 | {
15 | $this->resolution = $resolution;
16 | }
17 |
18 | public function calculate(int $from, int $to): int
19 | {
20 | return $this->roundInterval(($to - $from) / $this->resolution);
21 | }
22 |
23 | private function roundInterval(int $interval): int
24 | {
25 | // 2s
26 | if ($interval < 2) {
27 | return 1; // 1s
28 | }
29 | // 4s
30 | if ($interval < 4) {
31 | return 2; // 2s
32 | }
33 | // 8s
34 | if ($interval < 8) {
35 | return 5; // 5s
36 | }
37 | // 13s
38 | if ($interval < 13) {
39 | return 10; // 10s
40 | }
41 | // 18s
42 | if ($interval < 18) {
43 | return 15; // 15s
44 | }
45 | // 25s
46 | if ($interval < 25) {
47 | return 20; // 20s
48 | }
49 | // 45s
50 | if ($interval < 45) {
51 | return 30; // 30s
52 | }
53 | // 1.5m
54 | if ($interval < 90) {
55 | return 60; // 1m
56 | }
57 | // 3.5m
58 | if ($interval < 120) {
59 | return 120; // 2m
60 | }
61 | // 7.5m
62 | if ($interval < 450) {
63 | return 300; // 5m
64 | }
65 | // 12.5m
66 | if ($interval < 750) {
67 | return 600; // 10m
68 | }
69 | // 17.5m
70 | if ($interval < 1050) {
71 | return 900; // 15m
72 | }
73 | // 25m
74 | if ($interval < 1500) {
75 | return 1200; // 20m
76 | }
77 | // 45m
78 | if ($interval < 2700) {
79 | return 1800; // 30m
80 | }
81 | // 1.5h
82 | if ($interval < 5400) {
83 | return 3600; // 1h
84 | }
85 | // 2.5h
86 | if ($interval < 9000) {
87 | return 7200; // 2h
88 | }
89 | // 4.5h
90 | if ($interval < 16200) {
91 | return 10800; // 3h
92 | }
93 | // 9h
94 | if ($interval < 32400) {
95 | return 21600; // 6h
96 | }
97 | // 1d
98 | if ($interval < 86400) {
99 | return 43200; // 12h
100 | }
101 | // 1w
102 | if ($interval < 604800) {
103 | return 86400; // 1d
104 | }
105 | // 3w
106 | if ($interval < 1814400) {
107 | return 604800; // 1w
108 | }
109 | // 6w
110 | if ($interval < 3628800) {
111 | return 2592000; // 30d
112 | }
113 |
114 | return 31536000; // 1y
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/lib/utils/webpackencoreloader.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class WebpackEncoreLoader
14 | {
15 | /** @var string */
16 | private $base;
17 | /** @var array */
18 | private $entryPoints;
19 |
20 | public function __construct(string $base = 'bitrix/js/bsi.queue')
21 | {
22 | $this->base = $base;
23 | }
24 |
25 | public function load(string $entryName): void
26 | {
27 | if ($this->entryPoints === null) {
28 | $this->loadEntryPoints();
29 | }
30 |
31 | if (!isset($this->entryPoints['entrypoints'][$entryName])) {
32 | throw new InvalidArgumentException(sprintf('Entry with name "%s" not found.', $entryName));
33 | }
34 |
35 | $entry = $this->entryPoints['entrypoints'][$entryName];
36 | if (isset($entry['css']) && is_array($entry['css'])) {
37 | foreach ($entry['css'] as $path) {
38 | Asset::getInstance()->addString(
39 | '',
40 | true,
41 | AssetLocation::AFTER_CSS
42 | );
43 | }
44 | }
45 | if (isset($entry['js']) && is_array($entry['js'])) {
46 | foreach ($entry['js'] as $path) {
47 | Asset::getInstance()->addString(
48 | '',
49 | true,
50 | AssetLocation::BODY_END
51 | );
52 | }
53 | }
54 | }
55 |
56 |
57 | private function loadEntryPoints(): void
58 | {
59 | $entryPoints = json_decode(file_get_contents(
60 | realpath($_SERVER['DOCUMENT_ROOT'] . '/' . $this->base) . '/entrypoints.json'
61 | ), true);
62 |
63 | if ($entryPoints === false) {
64 | throw new RuntimeException('File "entrypoints.json" not found.');
65 | }
66 |
67 | $this->entryPoints = $entryPoints;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/options.php:
--------------------------------------------------------------------------------
1 | IsAdmin()) {
17 | $APPLICATION->AuthForm(Loc::getMessage('ACCESS_DENIED'));
18 | }
19 |
20 | $request = Application::getInstance()->getContext()->getRequest();
21 |
22 | $tabs = [
23 | [
24 | 'DIV' => 'edit1',
25 | 'TAB' => Loc::getMessage('MAIN_TAB_SET'),
26 | 'TITLE' => Loc::getMessage('MAIN_TAB_TITLE_SET'),
27 | 'ICON' => '',
28 | ],
29 | ];
30 | $tabControl = new CAdminTabControl('tabControl', $tabs);
31 |
32 | $allOptions = [
33 | [
34 | 'id' => 'stats_lifetime',
35 | 'name' => Loc::getMessage('BSI_QUEUE_OPTION_STATS_LIFETIME'),
36 | 'type' => 'integer',
37 | ],
38 | ];
39 |
40 | foreach ($allOptions as &$option) {
41 | $option['value'] = Option::get($module_id, $option['id'], null);
42 | }
43 | unset($option);
44 |
45 | $errorMessage = '';
46 | if ($request->isPost() && $request['Update'] !== '' && check_bitrix_sessid()) {
47 | foreach ($allOptions as $option) {
48 | $value = $request[$option['id']] ?? null;
49 |
50 | if ($option['type'] === 'integer') {
51 | $value = (int) $value;
52 | } elseif (!is_string($value)) {
53 | $value = trim($value);
54 | }
55 |
56 | Option::set($mid, $option['id'], $value);
57 | }
58 |
59 | if ($e = $APPLICATION->getException()) {
60 | CAdminMessage::ShowMessage([
61 | 'DETAILS' => $e->getString(),
62 | 'TYPE' => 'ERROR',
63 | 'HTML' => true,
64 | ]);
65 | } elseif ($request['back_url_settings'] !== '') {
66 | LocalRedirect($request['back_url_settings']);
67 | } else {
68 | LocalRedirect($APPLICATION->GetCurPage() . '?' . http_build_query([
69 | 'mid' => $mid,
70 | 'lang' => LANGUAGE_ID,
71 | 'back_url_settings' => $request['back_url_settings'],
72 | ]) . '&' . $tabControl->ActiveTabParam());
73 | }
74 | }
75 | ?>
76 |
101 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "encore dev --watch",
5 | "build": "encore production --progress",
6 | "lint": "eslint 'assets/**/*.{js,vue}' --quiet --fix"
7 | },
8 | "devDependencies": {
9 | "@babel/core": "^7.23.2",
10 | "@babel/preset-env": "^7.23.2",
11 | "@symfony/webpack-encore": "^4.6.0",
12 | "apexcharts": "^3.24.0",
13 | "autoprefixer": "^10.4.17",
14 | "axios": "^1.6.0",
15 | "babel-eslint": "^10.1.0",
16 | "babel-plugin-component": "^1.1.1",
17 | "cache-loader": "^4.1.0",
18 | "core-js": "^3.6.5",
19 | "element-ui": "^2.13.2",
20 | "eslint": "^7.32.0",
21 | "eslint-loader": "^4.0.2",
22 | "eslint-plugin-import": "^2.28.1",
23 | "eslint-plugin-vue": "^9.17.0",
24 | "husky": "^4.2.5",
25 | "lint-staged": "^10.2.11",
26 | "phpunserialize": "^1.0.1",
27 | "postcss-loader": "^7.3.3",
28 | "qs": "^6.9.4",
29 | "sass": "^1.69.4",
30 | "sass-loader": "^13.3.2",
31 | "vue": "^2.7.14",
32 | "vue-apexcharts": "^1.6.0",
33 | "vue-i18n": "^8.18.2",
34 | "vue-json-pretty": "^1.6.5",
35 | "vue-loader": "^15.11.1",
36 | "vue-template-compiler": "^2.6.11",
37 | "webpack": "^5.89.0",
38 | "webpack-cli": "^5.1.4",
39 | "yarn-audit-fix": "^10.0.1"
40 | },
41 | "eslintConfig": {
42 | "root": true,
43 | "parserOptions": {
44 | "ecmaVersion": 2020,
45 | "sourceType": "module",
46 | "parser": "babel-eslint"
47 | },
48 | "env": {
49 | "browser": true,
50 | "es6": true,
51 | "node": true
52 | },
53 | "extends": [
54 | "eslint:recommended",
55 | "plugin:vue/recommended"
56 | ],
57 | "rules": {
58 | "vue/html-indent": [
59 | "error",
60 | 4
61 | ],
62 | "vue/multi-word-component-names": "off"
63 | }
64 | },
65 | "postcss": {
66 | "plugins": {
67 | "autoprefixer": {}
68 | }
69 | },
70 | "husky": {
71 | "hooks": {
72 | "pre-commit": "lint-staged"
73 | }
74 | },
75 | "lint-staged": {
76 | "assets/**/*.{js,vue}": [
77 | "yarn run lint"
78 | ]
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | lib
12 | tests
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/prolog.php:
--------------------------------------------------------------------------------
1 | {
19 | config.plugins.push([
20 | 'component', {
21 | libraryName: 'element-ui',
22 | styleLibraryName: 'theme-chalk'
23 | }]);
24 | }, {
25 | useBuiltIns: 'usage',
26 | corejs: 3
27 | })
28 | .enableVueLoader(() => {}, { runtimeCompilerBuild: false })
29 | .enableSassLoader()
30 | .enablePostCssLoader()
31 | .addLoader({
32 | enforce: 'pre',
33 | test: /\.(js|vue)$/,
34 | loader: 'eslint-loader',
35 | exclude: /node_modules/,
36 | options: {
37 | emitError: true,
38 | emitWarning: true
39 | }
40 | })
41 | .addExternals({
42 | bitrix: 'BX'
43 | });
44 |
45 | module.exports = Encore.getWebpackConfig();
46 |
--------------------------------------------------------------------------------