├── .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 | Build Status 3 | Total Downloads 4 | Latest Stable Version 5 | License 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 | 11 | 12 | -------------------------------------------------------------------------------- /assets/components/AppTimePicker.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /assets/components/ChartPanel.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /assets/components/DashboardPanel.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /assets/components/MessageDetails.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | -------------------------------------------------------------------------------- /assets/components/RecentMessagesPanel.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 170 | -------------------------------------------------------------------------------- /assets/components/StatPanel.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | ![Дашборд](images/dashboard.png) 6 | 7 | ![Детализация по сообщению](images/message_details.png) 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 |
23 | 24 | 25 |
-------------------------------------------------------------------------------- /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 |
8 | 9 | 10 | 11 | 12 | 13 |

14 |

15 | 16 | 17 |

18 | 19 |
-------------------------------------------------------------------------------- /install/unstep2.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 | 16 | 17 |
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 |
78 | Begin(); 81 | $tabControl->BeginNextTab(); 82 | if ($errorMessage) : ?> 83 | 84 | 85 | 86 | 87 | 88 | 89 | : 90 | 91 | 92 | 93 | 94 | 95 | Buttons() ?> 97 | 98 | 99 | End() ?> 100 |
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 | --------------------------------------------------------------------------------