From 547092f3e16eda0281833f79a63dc648e9b66d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20W=C3=B3jcik?= Date: Thu, 15 Dec 2016 12:44:46 +0100 Subject: [PATCH] Track failure group retry request progress (#382) * Implement tracking failure group retry request progress * Updating minimum sc version to 1.30 --- gitversionconfig.yaml | 3 +- .../ServicePulse.Host.Tests.csproj | 1 + src/ServicePulse.Host.Tests/SpecsRunner.html | 1 + .../js/views/failed_groups/controller.spec.js | 331 ++++++++++++++++++ src/ServicePulse.Host/app/css/particular.css | 232 +++++++++++- src/ServicePulse.Host/app/js/app.constants.js | 1 + .../app/js/app.controller.js | 20 +- .../app/js/directives/moment.js | 5 +- .../ui.particular.failedMessageTabs.js | 17 +- .../ui.particular.failedMessageTabs.tpl.html | 2 +- .../ui.particular.productVersion.js | 1 + .../ui.particular.productVersion.tpl.html | 4 +- .../app/js/services/factory.shareddata.js | 6 +- .../app/js/services/service.toast.js | 4 +- .../js/services/services.service-control.js | 45 ++- .../app/js/views/archive/controller.js | 4 - .../app/js/views/dashboard/dashboard.html | 2 +- .../app/js/views/endpoints/endpoints.html | 8 +- .../event_log_items/eventLogItems.tpl.html | 2 +- .../app/js/views/failed_groups/controller.js | 294 ++++++++++++---- .../app/js/views/failed_groups/view.html | 210 ++++++++--- .../app/js/views/failed_messages/view.html | 4 +- src/SmokeTest.Client/Program.cs | 4 +- 23 files changed, 1026 insertions(+), 175 deletions(-) create mode 100644 src/ServicePulse.Host.Tests/tests/js/views/failed_groups/controller.spec.js diff --git a/gitversionconfig.yaml b/gitversionconfig.yaml index 4efe93410..e1a0c42e6 100644 --- a/gitversionconfig.yaml +++ b/gitversionconfig.yaml @@ -1 +1,2 @@ -assembly-versioning-scheme: MajorMinorPatch \ No newline at end of file +assembly-versioning-scheme: MajorMinorPatch +next-version: 1.7.0 \ No newline at end of file diff --git a/src/ServicePulse.Host.Tests/ServicePulse.Host.Tests.csproj b/src/ServicePulse.Host.Tests/ServicePulse.Host.Tests.csproj index 6435f9308..dbd79228b 100644 --- a/src/ServicePulse.Host.Tests/ServicePulse.Host.Tests.csproj +++ b/src/ServicePulse.Host.Tests/ServicePulse.Host.Tests.csproj @@ -81,6 +81,7 @@ + diff --git a/src/ServicePulse.Host.Tests/SpecsRunner.html b/src/ServicePulse.Host.Tests/SpecsRunner.html index f9871cceb..58831f62f 100644 --- a/src/ServicePulse.Host.Tests/SpecsRunner.html +++ b/src/ServicePulse.Host.Tests/SpecsRunner.html @@ -136,6 +136,7 @@ + diff --git a/src/ServicePulse.Host.Tests/tests/js/views/failed_groups/controller.spec.js b/src/ServicePulse.Host.Tests/tests/js/views/failed_groups/controller.spec.js new file mode 100644 index 000000000..9e44e0414 --- /dev/null +++ b/src/ServicePulse.Host.Tests/tests/js/views/failed_groups/controller.spec.js @@ -0,0 +1,331 @@ +describe('failedMessageGroupsController', + function() { + beforeEach(module('sc')); + + var $controller; + + beforeEach(inject(function(_$controller_) { + $controller = _$controller_; + })); + + describe('mark group as being processed', + function() { + var controller, serviceControlService, root, notifyService; + + beforeEach(inject(function ($rootScope, notifyService, $q) { + root = $rootScope; + this.notifyService = notifyService; + serviceControlService = { getExceptionGroups: function () { }, getHistoricGroups: function () { }, getExceptionGroupClassifiers: function() {} }; + var deferred = $q.defer(); + spyOn(serviceControlService, 'getExceptionGroups').and.callFake(function () { + return deferred.promise; + }); + spyOn(serviceControlService, 'getHistoricGroups').and.callFake(function () { + return deferred.promise; + }); + + spyOn(serviceControlService, 'getExceptionGroupClassifiers').and.callFake(function () { + return deferred.promise; + }); + + controller = $controller('failedMessageGroupsController', + { + $scope: root, + $timeout: null, + $interval: function(){}, + $location: null, + sharedDataService: { getstats: function() { return { number_of_pending_retries: 0 }; } }, + notifyService: notifyService, + serviceControlService: serviceControlService, + failedMessageGroupsService: null + }); + })); + + it('when an event is pubslished groups is marked as in progress', + function () { + + spyOn(root, '$broadcast'); + + controller.exceptionGroups = [ + { id: 1, workflow_state: null }, { id: 2, workflow_state: null }, + { id: 3, workflow_state: null } + ]; + + this.notifyService() + .notify("RetryOperationWaiting", + { + request_id: 1, + progress: { + percentage: 0.3, + messages_remaining: 2 + } + }); + + expect(controller.exceptionGroups[0].workflow_state.status).toEqual('waiting'); + expect(controller.exceptionGroups[0].workflow_state.total).toEqual(30); + expect(controller.exceptionGroups[1].workflow_state).toEqual(null); + expect(controller.exceptionGroups[2].workflow_state).toEqual(null); + }); + + }); + + describe('report groups progress', + function () { + var controller, serviceControlService, root; + + beforeEach(inject(function ($rootScope, notifyService, $q) { + root = $rootScope; + this.notifyService = notifyService; + serviceControlService = { getExceptionGroups: function () { }, getHistoricGroups: function () { }, getExceptionGroupClassifiers: function () { } }; + var deferred = $q.defer(); + spyOn(serviceControlService, 'getExceptionGroups').and.callFake(function () { + return deferred.promise; + }); + spyOn(serviceControlService, 'getHistoricGroups').and.callFake(function () { + return deferred.promise; + }); + + spyOn(serviceControlService, 'getExceptionGroupClassifiers').and.callFake(function () { + return deferred.promise; + }); + + controller = $controller('failedMessageGroupsController', + { + $scope: root, + $timeout: null, + $interval: function () { }, + $location: null, + sharedDataService: { getstats: function () { return { number_of_pending_retries: 0 }; } }, + notifyService: notifyService, + serviceControlService: serviceControlService, + failedMessageGroupsService: null + }); + })); + + it('when an event RetryOperationForwarding is pubslished group get its state updated', + function () { + + spyOn(root, '$broadcast'); + + controller.exceptionGroups = [ + { id: 1, workflow_state: {state: "in_progress", total:10} }, { id: 2, workflow_state: null }, + { id: 3, workflow_state: null } + ]; + + this.notifyService().notify("RetryOperationForwarding", { + request_id: 1, progress: { + percentage: 0.6, + messages_remaining: 2 + } + }); + + expect(controller.exceptionGroups[0].workflow_state.status).toEqual('forwarding'); + expect(controller.exceptionGroups[0].workflow_state.total).toEqual(60); + expect(controller.exceptionGroups[1].workflow_state).toEqual(null); + expect(controller.exceptionGroups[2].workflow_state).toEqual(null); + }); + + it('when an event RetryOperationForwarded is pubslished group get its state updated', + function () { + + spyOn(root, '$broadcast'); + + controller.exceptionGroups = [ + { id: 1, workflow_state: { state: "in_progress", total: 10 } }, { id: 2, workflow_state: null }, + { id: 3, workflow_state: null } + ]; + + this.notifyService().notify("RetryOperationForwarded", { + request_id: 1, progress: { + percentage: 0.7, + messages_remaining: 2 + } + }); + + expect(controller.exceptionGroups[0].workflow_state.status).toEqual('forwarding'); + expect(controller.exceptionGroups[0].workflow_state.total).toEqual(70); + expect(controller.exceptionGroups[1].workflow_state).toEqual(null); + expect(controller.exceptionGroups[2].workflow_state).toEqual(null); + }); + + + it('when an event RetryOperationPreparing is pubslished group get its state updated', + function () { + + spyOn(root, '$broadcast'); + + controller.exceptionGroups = [ + { id: 1, workflow_state: { state: "in_progress", total: 10 } }, { id: 2, workflow_state: null }, + { id: 3, workflow_state: null } + ]; + + this.notifyService().notify("RetryOperationPreparing", { + request_id: 1, progress: { + percentage: 0.5, + messages_remaining: 2 + } + }); + + expect(controller.exceptionGroups[0].workflow_state.status).toEqual('preparing'); + expect(controller.exceptionGroups[0].workflow_state.total).toEqual(50); + expect(controller.exceptionGroups[1].workflow_state).toEqual(null); + expect(controller.exceptionGroups[2].workflow_state).toEqual(null); + }); + + }); + + describe('report groups processing finished', + function () { + var controller, serviceControlService, root; + + beforeEach(inject(function ($rootScope, notifyService, $q) { + root = $rootScope; + this.notifyService = notifyService; + serviceControlService = { getExceptionGroups: function () { }, getHistoricGroups: function () { }, getExceptionGroupClassifiers: function () { } }; + var deferred = $q.defer(); + spyOn(serviceControlService, 'getExceptionGroups').and.callFake(function () { + return deferred.promise; + }); + spyOn(serviceControlService, 'getHistoricGroups').and.callFake(function () { + return deferred.promise; + }); + + spyOn(serviceControlService, 'getExceptionGroupClassifiers').and.callFake(function () { + return deferred.promise; + }); + + controller = $controller('failedMessageGroupsController', + { + $scope: root, + $timeout: null, + $interval: function () { }, + $location: null, + sharedDataService: { getstats: function () { return { number_of_pending_retries: 0 }; } }, + notifyService: notifyService, + serviceControlService: serviceControlService, + failedMessageGroupsService: null + }); + })); + + it('when an event is pubslished group get its state updated', + function () { + + spyOn(root, '$broadcast'); + + controller.exceptionGroups = [ + { id: 1, workflow_state: { state: "in_progress", total: 10, count: 3 } }, { id: 2, workflow_state: null }, + { id: 3, workflow_state: null } + ]; + + this.notifyService().notify("RetryOperationCompleted", { + request_id: 1, progress: { + percentage: 1, + messages_remaining: 0 + } + }); + + expect(controller.exceptionGroups[0].workflow_state.status).toEqual('completed'); + expect(controller.exceptionGroups[1].workflow_state).toEqual(null); + expect(controller.exceptionGroups[2].workflow_state).toEqual(null); + }); + + }); + + describe('update exception groups', + function () { + var controller, serviceControlService, root, deferred; + + beforeEach(inject(function ($rootScope, notifyService, $q) { + root = $rootScope; + this.notifyService = notifyService; + serviceControlService = { getExceptionGroups: function () { }, getHistoricGroups: function () { }, getExceptionGroupClassifiers: function () { } }; + deferred = $q.defer(); + var emptyDefer = $q.defer(); + spyOn(serviceControlService, 'getExceptionGroups').and.callFake(function () { + return deferred.promise; + }); + spyOn(serviceControlService, 'getHistoricGroups').and.callFake(function () { + return emptyDefer.promise; + }); + + spyOn(serviceControlService, 'getExceptionGroupClassifiers').and.callFake(function () { + return emptyDefer.promise; + }); + + controller = $controller('failedMessageGroupsController', + { + $scope: root, + $timeout: null, + $interval: function () { }, + $location: null, + sharedDataService: { getstats: function () { return { number_of_pending_retries: 0 }; } }, + notifyService: notifyService, + serviceControlService: serviceControlService, + failedMessageGroupsService: null + }); + })); + + it('add new group to an empty set', + function () { + controller.exceptionGroups = []; + + controller.updateExceptionGroups(); + root.$apply(function () { + deferred.resolve({ data: [{ id: 3, workflow_state: null }] }); + }); + expect(controller.exceptionGroups[0].id).toEqual(3); + expect(controller.exceptionGroups[0].workflow_state).toBeDefined(); + + }); + + it('update a group', + function () { + controller.exceptionGroups = [{id: 3, workflow_state: null }]; + + controller.updateExceptionGroups(); + root.$apply(function () { + deferred.resolve({ data: [{ id: 3, workflow_state: null, retry_status: 'pending' }] }); + }); + expect(controller.exceptionGroups[0].id).toEqual(3); + expect(controller.exceptionGroups[0].workflow_state).toBeDefined(); + expect(controller.exceptionGroups[0].retry_status).toEqual('pending'); + + }); + + it('delete a group', + function () { + controller.exceptionGroups = [{ id: 3, workflow_state: null }]; + + controller.updateExceptionGroups(); + root.$apply(function () { + //deferred.resolve({data: []}); + deferred.resolve({ data: [] }); + }); + expect(controller.exceptionGroups.length).toEqual(0); + }); + + it('adds a group, delete and updates ', + function () { + controller.exceptionGroups = [{ id: 3, workflow_state: null }, {id: 4}]; + + controller.updateExceptionGroups(); + root.$apply(function () { + deferred.resolve({ + data: [{ id: 1, workflow_state: null, retry_status: 'forwarding' }, { id: 2, workflow_state: null, retry_status: 'completed' }, + { id: 3, workflow_state: null, retry_status: 'pending' }] + }); + deferred.resolve({ data: [] }); + }); + expect(controller.exceptionGroups.length).toEqual(3); + expect(controller.exceptionGroups[0].id).toEqual(3); + expect(controller.exceptionGroups[0].workflow_state).toBeDefined(); + expect(controller.exceptionGroups[0].retry_status).toEqual('pending'); + expect(controller.exceptionGroups[1].id).toEqual(1); + expect(controller.exceptionGroups[1].workflow_state).toBeDefined(); + expect(controller.exceptionGroups[1].retry_status).toEqual('forwarding'); + expect(controller.exceptionGroups[2].id).toEqual(2); + expect(controller.exceptionGroups[2].workflow_state).toBeDefined(); + expect(controller.exceptionGroups[2].retry_status).toEqual('completed'); + }); + }); + }); \ No newline at end of file diff --git a/src/ServicePulse.Host/app/css/particular.css b/src/ServicePulse.Host/app/css/particular.css index 8b1f0c5e9..71d957354 100644 --- a/src/ServicePulse.Host/app/css/particular.css +++ b/src/ServicePulse.Host/app/css/particular.css @@ -7,8 +7,12 @@ a { color: #00a3c4; font-weight: bold; + outline: none; + border: none; } +a:focus, button:focus {outline:0 !important;} + a:hover { color: #00a3c4; font-weight: bold; @@ -156,7 +160,7 @@ footer a:hover { font-weight: normal; } .warning i { color: #aa6708; } -.danger { color: #ce4844; } +.danger { color: #ce4844 !important; font-weight: bold !important; } .box-info { border-left-color: #1b809e; } @@ -175,8 +179,6 @@ footer a:hover { font-weight: normal; } padding: 0; } -.box-body { } - /* fixed bus with repeat list transition lag */ .ng-animate { transition: none; } @@ -356,7 +358,7 @@ section.events > div > div > div > div { padding-bottom: 8px !important; } .lead { -ms-word-wrap: break-word; color: #181919 !important; - font-size: 16px !important; + font-size: 14px !important; font-weight: bold; margin-bottom: 3px; word-wrap: break-word; @@ -408,12 +410,16 @@ h3 { font-weight: bold; } -.box:hover, .summary-item:hover { +.summary-item:hover { background-color: #edf6f7; border-color: #00a3c4 !important; cursor: pointer; } +.check-hover { + +} + h5 { color: #777f7f; display: inline-block !important; @@ -424,6 +430,16 @@ h5 { text-transform: uppercase; } +h6 { + font-size: 18px; + font-weight: bold; + color: #181919; +} + +h6 a:hover { + cursor: pointer; +} + .tabs { border-bottom: 1px solid #ced6d3; padding: 0; @@ -444,7 +460,7 @@ h5 { .tabs h5.active a { color: #181919; } .tabs a { - color: #a8b3b1; + color: #929e9e; cursor: pointer; } @@ -485,11 +501,14 @@ h1 { margin: 0 0 32px; } -p.metadata { margin-bottom: 6px; } +p.metadata { + margin-bottom: 6px; +} span.metadata { display: inline-block; padding: 0px 20px 2px 0; + color: #777f7f; } .metadata:first-child { padding-left: 0; } @@ -746,13 +765,27 @@ p.endpoint-metadata { .fa-stack-2x { font-size: 24px; } -.events .box { padding-bottom: 0; } +.events .box { + padding-bottom: 0; +} + +.events .box:hover { + cursor: pointer; + background-color: #edf6f7; + border: 1px solid #00a3c4; +} .events p.lead { padding-bottom: 10px; } .fake-link { - color: #00a3c4; - text-decoration: none; + color: #00a3c4 !important; + text-decoration: none !important; +} + + +.fake-link:hover { + color: #00a3c4 !important; + text-decoration: none !important; } .version-info-container { width: 100% !important; } @@ -772,6 +805,10 @@ p.endpoint-metadata { margin: 0 15px; } +.list-section { + margin-top: 20px; +} + /* Add animation */ @@ -840,8 +877,8 @@ nav { width: 100% !important; } -.filter-period-menu, .sort-menu { - float:right; +.filter-period-menu, .sort-menu, .msg-group-menu { + float: right; } .filter-input, .action-btns, .filter-period-menu, .sort-menu { @@ -858,6 +895,11 @@ nav { margin: 0 30px 0 6px; } +.msg-group-menu { + margin: 21px 0px 0 6px; + padding-right: 15px; +} + .sort-menu { margin-left: 6px; padding-top: 8px; @@ -905,11 +947,157 @@ p.lead hard-wrap.ng-binding { color: #777f7f; } +.collapsible-section { + margin-top: 20px; +} +.disclose-link { + margin-top: 20px; +} + +.extra-box-padding { + padding-left: 20px !important; + padding-right: 20px !important; +} + +.progress { + margin-bottom: 10px; +} + +.progress-bar { + background-color: #777f7f; + display: inline-block; +} + .no-side-padding { - padding-right: 0; - padding-left: 0; + padding-right: 0; + padding-left: 0; +} + +..box-no-interaction { + background-color: #fff; + border-color: #eee !important; +} + +.box-no-interaction:hover { + background-color: #fff !important; + border-color: #eee !important; + cursor: default; } +.no-link-underline { + color: #00a3c4; + text-decoration: none; +} + +.panel-body ul { + list-style: none; + padding-left: 0; +} + + + +ul.retry-request-progress li > div { + margin-bottom: 6px; +} + +li.left-to-do, li.completed { + color: #B0B5B5; +} + +li.left-to-do { + padding-left: 15px; +} + +li.active div { + color: #fff !important; + font-weight: bold; +} + +li.active div.bulk-retry-progress-status:before { + font: normal normal normal 14px/1 FontAwesome; + content: "\f061 \00a0"; +} + +div.retry-completed.bulk-retry-progress-status { + color: #fff; + font-weight: bold; +} + +li.completed div.bulk-retry-progress-status:before, div.retry-completed.bulk-retry-progress-status:before { + font: normal normal normal 14px/1 FontAwesome; + content: "\f00c \00a0"; +} + +div.col-xs-3.col-sm-3.retry-op-queued { + color: #B0B5B5 !important; +} + +div.progress-bar.progress-bar-striped.active { + color: #fff !important; +} + +.progress.bulk-retry-progress { + margin-bottom: 0; + background-color: #333333; +} + +.retry-completed, ul.retry-request-progress button { + display: inline-block; +} + +ul.retry-request-progress button { + background-color: #00a3c4; +} + +.panel-retry { + background-color: #1A1A1A; + border: none; + color: #fff +} + +.panel-retry p.lead { + color: #fff; +} + +.navbar-inverse { + background-color: #1A1A1A; +} + +.panel-retry span.metadata, .panel-retry sp-moment { + color: #B0B5B5 !important; +} + +span.short-group-history { + text-align: center; + display: inherit; + margin-top: 10px; + color: #777f7f; +} + +div.danger.sc-restart-warning { + margin-top: 3px; +} + +div.danger.sc-restart-warning, div.danger.sc-restart-warning > i { + color: #fa733d !important; + font-weight: normal !important; + letter-spacing: 0.1px; +} + +div.danger.sc-restart-warning > strong { + letter-spacing: 0.2px; +} + +.op-metadata { + border-top: 1px solid #414242; + padding-top: 15px; +} + + + + +/* RESPONSIVE TWEAKS */ + @media only screen and (min-width: 993px) { .filter-period-menu { margin-right: 0px; @@ -918,7 +1106,7 @@ p.lead hard-wrap.ng-binding { @media only screen and (max-width: 992px) { .filter-period-menu, .sort-menu { - float: left !important; + float: left !important; } .sort-menu { margin-top: 0; @@ -933,6 +1121,11 @@ p.lead hard-wrap.ng-binding { .filter-toolbar .input-group { margin-bottom: 6px; } + .msg-group-menu { + float: left !important; + margin-top: 0; + margin-left: 15px; + } div.sp-pull-right { display: inline-block; float: none; @@ -947,9 +1140,12 @@ p.lead hard-wrap.ng-binding { .input-group-btn button.btn.btn-default { margin-right: 0; } + .no-mobile-side-padding { + padding-right: 0; + padding-left: 0; + } } - @media only screen and (max-width: 480px) { .sidebar-label { margin: 3px 0 14px; } @@ -970,7 +1166,9 @@ p.lead hard-wrap.ng-binding { float: none; margin-top: 4px; } - + .progress.bulk-retry-progress { + margin-top: 6px; + } } @media only screen and (max-width: 320px) { diff --git a/src/ServicePulse.Host/app/js/app.constants.js b/src/ServicePulse.Host/app/js/app.constants.js index 33c9448de..c15230661 100644 --- a/src/ServicePulse.Host/app/js/app.constants.js +++ b/src/ServicePulse.Host/app/js/app.constants.js @@ -2,6 +2,7 @@ angular.module('sc') .constant('version', '1.2.0') + .constant('showPendingRetry', false) .constant('scConfig', { service_control_url: 'http://localhost:33333/api/' }); diff --git a/src/ServicePulse.Host/app/js/app.controller.js b/src/ServicePulse.Host/app/js/app.controller.js index 87d094777..b43d741e5 100644 --- a/src/ServicePulse.Host/app/js/app.controller.js +++ b/src/ServicePulse.Host/app/js/app.controller.js @@ -206,8 +206,26 @@ listener.subscribe($scope, function (message) { notifier.notify("FailedMessageGroupArchived", message); }, "FailedMessageGroupArchived"); - + listener.subscribe($scope, function (message) { + notifier.notify("RetryOperationWaiting", message); + }, "RetryOperationWaiting"); + + listener.subscribe($scope, function (message) { + notifier.notify("RetryOperationPreparing", message); + }, "RetryOperationPreparing"); + + listener.subscribe($scope, function (message) { + notifier.notify("RetryOperationForwarding", message); + }, "RetryOperationForwarding"); + + listener.subscribe($scope, function (message) { + notifier.notify("RetryOperationForwarded", message); + }, "RetryOperationForwarded"); + + listener.subscribe($scope, function (message) { + notifier.notify("RetryOperationCompleted", message); + }, "RetryOperationCompleted"); }; controller.$inject = [ diff --git a/src/ServicePulse.Host/app/js/directives/moment.js b/src/ServicePulse.Host/app/js/directives/moment.js index a92f6db41..093c6e213 100644 --- a/src/ServicePulse.Host/app/js/directives/moment.js +++ b/src/ServicePulse.Host/app/js/directives/moment.js @@ -5,6 +5,9 @@ function Directive($timeout, $moment) { return { restrict: 'E', + scope: { + emptyMessage: '@' + }, link: function(scope, element, attrs) { var minDate = '0001-01-01T00:00:00'; var timeoutId = null; @@ -28,7 +31,7 @@ updateText(); updateLoop(); } else { - element.text('unknown'); + element.text(scope.emptyMessage || 'unknown'); } }); diff --git a/src/ServicePulse.Host/app/js/directives/ui.particular.failedMessageTabs.js b/src/ServicePulse.Host/app/js/directives/ui.particular.failedMessageTabs.js index 48bbc31c5..f2926540e 100644 --- a/src/ServicePulse.Host/app/js/directives/ui.particular.failedMessageTabs.js +++ b/src/ServicePulse.Host/app/js/directives/ui.particular.failedMessageTabs.js @@ -2,7 +2,7 @@ 'use strict'; - function controller($scope, $interval, $location, sharedDataService, notifyService, serviceControlService) { + function controller($scope, $interval, $location, sharedDataService, notifyService, serviceControlService, showPendingRetry) { var notifier = notifyService(); @@ -11,8 +11,9 @@ }; var stats = sharedDataService.getstats(); + var currentClassification; var allFailedMessagesGroup = { 'id': undefined, 'title': 'All Failed Messages', 'count': stats.number_of_failed_messages } - + $scope.showPendingRetry = showPendingRetry; $scope.counters = { group: stats.number_of_exception_groups, message: stats.number_of_failed_messages, @@ -25,12 +26,6 @@ $location.path('/failedMessages'); } - var exceptionGroupCountUpdatedTimer = $interval(function() { - serviceControlService.getTotalExceptionGroups().then(function(response) { - notifier.notify('ExceptionGroupCountUpdated', response); - }); - }, 5000); - var archiveMessagesUpdatedTimer = $interval(function() { serviceControlService.getTotalArchivedMessages().then(function(response) { notifier.notify('ArchivedMessagesUpdated', response || 0); @@ -51,10 +46,6 @@ // Cancel interval on page changes $scope.$on('$destroy', function() { - if (angular.isDefined(exceptionGroupCountUpdatedTimer)) { - $interval.cancel(exceptionGroupCountUpdatedTimer); - exceptionGroupCountUpdatedTimer = undefined; - } if (angular.isDefined(archiveMessagesUpdatedTimer)) { $interval.cancel(archiveMessagesUpdatedTimer); archiveMessagesUpdatedTimer = undefined; @@ -88,7 +79,7 @@ }, 'PendingRetriesTotalUpdated'); } - controller.$inject = ['$scope', '$interval', '$location', 'sharedDataService', 'notifyService', 'serviceControlService']; + controller.$inject = ['$scope', '$interval', '$location', 'sharedDataService', 'notifyService', 'serviceControlService', 'showPendingRetry']; function directive() { return { diff --git a/src/ServicePulse.Host/app/js/directives/ui.particular.failedMessageTabs.tpl.html b/src/ServicePulse.Host/app/js/directives/ui.particular.failedMessageTabs.tpl.html index e1b25b1fd..a9f3c1cb9 100644 --- a/src/ServicePulse.Host/app/js/directives/ui.particular.failedMessageTabs.tpl.html +++ b/src/ServicePulse.Host/app/js/directives/ui.particular.failedMessageTabs.tpl.html @@ -4,7 +4,7 @@
Failed message groups ({{counters.group | number}})
All failed messages ({{counters.message | number}})
Archived messages ({{counters.archived | number}})
-
Pending retries ({{counters.pendingRetries | number}})
+
Pending retries ({{counters.pendingRetries | number}})
diff --git a/src/ServicePulse.Host/app/js/directives/ui.particular.productVersion.js b/src/ServicePulse.Host/app/js/directives/ui.particular.productVersion.js index 6415b84fa..24851a364 100644 --- a/src/ServicePulse.Host/app/js/directives/ui.particular.productVersion.js +++ b/src/ServicePulse.Host/app/js/directives/ui.particular.productVersion.js @@ -14,6 +14,7 @@ if (semverService.isUpgradeAvailable($scope.version, result.SP[0]['tag'])) { $scope.newspversion = true; $scope.newspversionlink = result.SP[0]['release']; + $scope.newspversionnumber = result.SP[0]['tag']; } } diff --git a/src/ServicePulse.Host/app/js/directives/ui.particular.productVersion.tpl.html b/src/ServicePulse.Host/app/js/directives/ui.particular.productVersion.tpl.html index b39b9449d..cd0b61b85 100644 --- a/src/ServicePulse.Host/app/js/directives/ui.particular.productVersion.tpl.html +++ b/src/ServicePulse.Host/app/js/directives/ui.particular.productVersion.tpl.html @@ -11,7 +11,7 @@ - ServicePulse v{{version}} ( update available) + ServicePulse v{{version}} ( v{{newspversionnumber}} available) @@ -31,7 +31,7 @@ - ServiceControl v{{scversion}} ( (Upgrade to v{{newscversionnumber}})) + ServiceControl v{{scversion}} ( v{{newscversionnumber}} available) diff --git a/src/ServicePulse.Host/app/js/services/factory.shareddata.js b/src/ServicePulse.Host/app/js/services/factory.shareddata.js index c67ed240b..dc6b1c4b0 100644 --- a/src/ServicePulse.Host/app/js/services/factory.shareddata.js +++ b/src/ServicePulse.Host/app/js/services/factory.shareddata.js @@ -34,7 +34,7 @@ var environment = { sc_version: undefined, - minimum_supported_sc_version: "1.27.0", + minimum_supported_sc_version: "1.30.0", is_compatible_with_sc: true, sp_version: spVersion }; @@ -69,10 +69,6 @@ stats.active_endpoints = stat.active || 0; }); - serviceControlService.getTotalExceptionGroups().then(function (response) { - notifier.notify('ExceptionGroupCountUpdated', response); - }); - serviceControlService.getTotalArchivedMessages().then(function (response) { notifier.notify('ArchivedMessagesUpdated', response || 0); }); diff --git a/src/ServicePulse.Host/app/js/services/service.toast.js b/src/ServicePulse.Host/app/js/services/service.toast.js index 38700af33..15b194487 100644 --- a/src/ServicePulse.Host/app/js/services/service.toast.js +++ b/src/ServicePulse.Host/app/js/services/service.toast.js @@ -14,8 +14,8 @@ }); } - this.showInfo = function (text) { - this.showToast(text, 'info', 'Info', false); + this.showInfo = function (text, title, sticky) { + this.showToast(text, 'info', title || 'Info', sticky); } this.showError = function (text) { diff --git a/src/ServicePulse.Host/app/js/services/services.service-control.js b/src/ServicePulse.Host/app/js/services/services.service-control.js index cd9beecbe..4bf4da181 100644 --- a/src/ServicePulse.Host/app/js/services/services.service-control.js +++ b/src/ServicePulse.Host/app/js/services/services.service-control.js @@ -38,11 +38,30 @@ }); } + var previousExceptionGroupEtag; + function getExceptionGroups(classifier) { var url = uri.join(scConfig.service_control_url, 'recoverability', 'groups', classifier); - return $http.get(url).then(function(response) { + return $http.get(url).then(function (response) { + var status = 200; + if (previousExceptionGroupEtag === response.headers('etag')) { + status = 304; + } else { + previousExceptionGroupEtag = response.headers('etag'); + } return { - data: response.data + data: response.data, + status: status + }; + }); + } + + function getHistoricGroups() { + var url = uri.join(scConfig.service_control_url, 'recoverability', 'history'); + return $http.get(url).then(function (response) { + return { + data: response.data, + etag: response.headers('etag') }; }); } @@ -75,13 +94,6 @@ }); } - function getTotalExceptionGroups() { - var url = uri.join(scConfig.service_control_url, 'recoverability', 'groups'); - return $http.head(url).then(function(response) { - return response.headers('Total-Count'); - }); - } - function getTotalFailedMessages() { var url = uri.join(scConfig.service_control_url, 'errors?status=unresolved'); return $http.head(url).then(function(response) { @@ -213,6 +225,16 @@ }); } + function acknowledgeGroup(id, successText, failureText) { + var url = uri.join(scConfig.service_control_url, 'recoverability', 'unacknowledgedgroups', id); + return $http.delete(url).then( + function () { + // notifications.pushForCurrentRoute(successText, 'info'); + }, function () { + notifications.pushForCurrentRoute('Retrying messages failed', 'danger'); + }); + } + function retryExceptionGroup(id, successText) { var url = uri.join(scConfig.service_control_url, 'recoverability', 'groups', id, 'errors', 'retry'); @@ -263,10 +285,10 @@ getFailedMessages: getFailedMessages, getExceptionGroups: getExceptionGroups, getExceptionGroupClassifiers: getExceptionGroupClassifiers, + getHistoricGroups: getHistoricGroups, getFailedMessagesForExceptionGroup: getFailedMessagesForExceptionGroup, getMessageBody: getMessageBody, getMessageHeaders: getMessageHeaders, - getTotalExceptionGroups: getTotalExceptionGroups, getTotalFailedMessages: getTotalFailedMessages, getTotalArchivedMessages: getTotalArchivedMessages, getTotalFailingCustomChecks: getTotalFailingCustomChecks, @@ -281,7 +303,8 @@ archiveExceptionGroup: archiveExceptionGroup, retryExceptionGroup: retryExceptionGroup, getHeartbeatStats: getHeartbeatStats, - loadQueueNames: loadQueueNames + loadQueueNames: loadQueueNames, + acknowledgeGroup: acknowledgeGroup }; return service; diff --git a/src/ServicePulse.Host/app/js/views/archive/controller.js b/src/ServicePulse.Host/app/js/views/archive/controller.js index 42595c6d6..a60cef3f3 100644 --- a/src/ServicePulse.Host/app/js/views/archive/controller.js +++ b/src/ServicePulse.Host/app/js/views/archive/controller.js @@ -178,10 +178,6 @@ .finally(function () { vm.selectedIds = []; }); - - serviceControlService.getTotalExceptionGroups().then(function (response) { - notifier.notify('ExceptionGroupCountUpdated', response); - }); }; vm.archiveExceptionGroup = function (group) { diff --git a/src/ServicePulse.Host/app/js/views/dashboard/dashboard.html b/src/ServicePulse.Host/app/js/views/dashboard/dashboard.html index 30308e9ab..fb162e724 100644 --- a/src/ServicePulse.Host/app/js/views/dashboard/dashboard.html +++ b/src/ServicePulse.Host/app/js/views/dashboard/dashboard.html @@ -13,7 +13,7 @@

Dashboard

-
system status
+
System status
diff --git a/src/ServicePulse.Host/app/js/views/endpoints/endpoints.html b/src/ServicePulse.Host/app/js/views/endpoints/endpoints.html index b04fdd0af..9024069bf 100644 --- a/src/ServicePulse.Host/app/js/views/endpoints/endpoints.html +++ b/src/ServicePulse.Host/app/js/views/endpoints/endpoints.html @@ -1,7 +1,7 @@
-
-

Endpoint Overview

+
+

Endpoints Overview

@@ -17,7 +17,7 @@
-
+
@@ -44,7 +44,7 @@
-
+
diff --git a/src/ServicePulse.Host/app/js/views/event_log_items/eventLogItems.tpl.html b/src/ServicePulse.Host/app/js/views/event_log_items/eventLogItems.tpl.html index 06ec6c10b..6e16612d4 100644 --- a/src/ServicePulse.Host/app/js/views/event_log_items/eventLogItems.tpl.html +++ b/src/ServicePulse.Host/app/js/views/event_log_items/eventLogItems.tpl.html @@ -2,7 +2,7 @@
-
Last 10 events
+
Last 10 events
diff --git a/src/ServicePulse.Host/app/js/views/failed_groups/controller.js b/src/ServicePulse.Host/app/js/views/failed_groups/controller.js index b107450b9..46dc56479 100644 --- a/src/ServicePulse.Host/app/js/views/failed_groups/controller.js +++ b/src/ServicePulse.Host/app/js/views/failed_groups/controller.js @@ -2,12 +2,15 @@ (function (window, angular, undefined) { "use strict"; - function createWorkflowState(optionalStatus, optionalMessage, optionalTotal, optionalCount) { + function createWorkflowState(optionalStatus, optionalMessage, optionalTotal, optionalFailed) { + if (optionalTotal && optionalTotal <= 1) { + optionalTotal = optionalTotal * 100; + } return { status: optionalStatus || 'working', message: optionalMessage || 'working', total: optionalTotal || 0, - count: optionalCount || 0 + failed: optionalFailed || false }; } @@ -19,7 +22,8 @@ sharedDataService, notifyService, serviceControlService, - failedMessageGroupsService) { + failedMessageGroupsService, + toastService) { var vm = this; var notifier = notifyService(); @@ -28,48 +32,81 @@ vm.exceptionGroups = []; vm.availableClassifiers = []; vm.selectedExceptionGroup = {}; - vm.allFailedMessagesGroup = { 'id': undefined, 'title': 'All Failed Messages', 'count': 0 } vm.stats = sharedDataService.getstats(); - vm.viewExceptionGroup = function (group) { + vm.viewExceptionGroup = function(group) { sharedDataService.set(group); $location.path('/failedMessages'); - } - - vm.archiveExceptionGroup = function (group) { - group.workflow_state = { status: 'working', message: 'working' }; - var response = failedMessageGroupsService.archiveGroup(group.id, 'Archive Group Request Enqueued', 'Archive Group Request Rejected') - .then(function (message) { - group.workflow_state = createWorkflowState('success', message); - notifier.notify('ArchiveGroupRequestAccepted', group); - - }, function (message) { - group.workflow_state = createWorkflowState('error', message); - notifier.notify('ArchiveGroupRequestRejected', group); - }) - .finally(function () { + }; - }); + vm.acknowledgeGroup = function (group) { + serviceControlService.acknowledgeGroup(group.id, + 'Group Acknowledged', + 'Acknowledging Group Failed') + .then(function(message) { + vm.exceptionGroups.splice(vm.exceptionGroups.indexOf(group), 1); + }); } - vm.retryExceptionGroup = function (group) { - group.workflow_state = { status: 'working', message: 'working' }; + vm.archiveExceptionGroup = function(group) { + group.workflow_state = { status: 'requestingArchive', message: 'Archive request initiated...' }; + failedMessageGroupsService.archiveGroup(group.id, + 'Archive Group Request Enqueued', + 'Archive Group Request Rejected') + .then(function(message) { + group.workflow_state = createWorkflowState('archiveRequested', 'Archiving messages...'); + notifier.notify('ArchiveGroupRequestAccepted', group); + + }, + function(message) { + group.workflow_state = createWorkflowState('error', message); + notifier.notify('ArchiveGroupRequestRejected', group); + }); + }; - var response = failedMessageGroupsService.retryGroup(group.id, 'Retry Group Request Enqueued', 'Retry Group Request Rejected') - .then(function (message) { - // We are going to have to wait for service control to tell us the job has been done - group.workflow_state = createWorkflowState('success', message); - notifier.notify('RetryGroupRequestAccepted', group); + vm.retryExceptionGroup = function(group) { + group.workflow_state = { status: 'waiting', message: getMessageForRetryStatus('waiting') }; - }, function (message) { - group.workflow_state = createWorkflowState('error', message); - notifier.notify('RetryGroupRequestRejected', group); - }) - .finally(function () { + failedMessageGroupsService.retryGroup(group.id, + 'Retry Group Request Enqueued', + 'Retry Group Request Rejected') + .then(function() { + // We are going to have to wait for service control to tell us the job has been done + notifier.notify('RetryGroupRequestAccepted', group); - }); + }, + function(message) { + group.workflow_state = createWorkflowState('error', message); + notifier.notify('RetryGroupRequestRejected', group); + }); + }; + + + var statuses = ['waiting', 'preparing', 'queued', 'forwarding']; + vm.getClasses = function (stepStatus, currentStatus) { + if (currentStatus === 'queued') { + currentStatus = 'forwarding'; + } + var indexOfStep = statuses.indexOf(stepStatus); + var indexOfCurrent = statuses.indexOf(currentStatus); + if (indexOfStep > indexOfCurrent) { + return 'left-to-do'; + } + else if (indexOfStep === indexOfCurrent) { + return 'active'; + } else { + return 'completed'; + } } + vm.isBeingRetried = function(group) { + return group.workflow_state.status !== 'none' && (group.workflow_state.status !== 'completed' || group.need_user_acknowledgement === true) && !vm.isBeingArchived(group); + }; + + vm.isBeingArchived = function (group) { + return group.workflow_state.status === 'requestingArchive' || group.workflow_state.status === 'archiveRequested'; + }; + vm.selectClassification = function (newClassification) { vm.loadingData = true; vm.selectedClassification = newClassification; @@ -97,58 +134,196 @@ }); }; - var autoGetExceptionGroups = function () { - vm.exceptionGroups = []; + function initializeGroupState(group) { + var nObj = group; + var retryStatus = (nObj.retry_status ? nObj.retry_status.toLowerCase() : null) || + 'none'; + if (retryStatus === 'preparing' && nObj.retry_progress === 1) { + retryStatus = 'queued'; + } + nObj + .workflow_state = + createWorkflowState(retryStatus, + getMessageForRetryStatus(retryStatus, nObj.retry_failed), + nObj.retry_progress, + nObj.retry_failed); + + return nObj; + }; + + vm.updateExceptionGroups = function () { return serviceControlService.getExceptionGroups(vm.selectedClassification) .then(function (response) { - if (response.data.length > 0) { + if (response.status === 304) { + return true; + } + var exceptionGroupsToBeRemoved = vm.exceptionGroups.filter(function(item) { + return !response.data.some(function(d) { + return d.id === item.id; + }); + }); + exceptionGroupsToBeRemoved.forEach(function(item) { + vm.exceptionGroups.splice(vm.exceptionGroups.indexOf(item), 1); + }); - vm.allFailedMessagesGroup.count = 0; + vm.exceptionGroups.forEach(function(group) { + var d = response.data.filter(function(item) { + return item.id === group.id; + })[0]; - // need a map in some ui state for controlling animations - vm.exceptionGroups = response.data.map(function (obj) { - vm.allFailedMessagesGroup.count += obj.count; - var nObj = obj; - nObj.workflow_state = createWorkflowState('ready', ''); - return nObj; + for (var prop in d) { + group[prop] = d[prop]; + } + initializeGroupState(group); + }); + + response.data.filter(function(group) { + return !vm.exceptionGroups.some(function(item) { + return item.id === group.id; + }); + }) + .forEach(function(group) { + vm.exceptionGroups.push(group); + initializeGroupState(group); }); + if (vm.exceptionGroups.length !== vm.stats.number_of_exception_groups) { vm.stats.number_of_exception_groups = vm.exceptionGroups.length; notifier.notify('ExceptionGroupCountUpdated', vm.stats.number_of_exception_groups); } + return true; + }); + } + + var autoGetExceptionGroups = function () { + vm.exceptionGroups = []; + return serviceControlService.getExceptionGroups(vm.selectedClassification) + .then(function (response) { + if (response.status === 304 && vm.exceptionGroups.length > 0) { + return true; + } + + if (response.data.length > 0) { + + // need a map in some ui state for controlling animations + vm.exceptionGroups = response.data.map(initializeGroupState); + + if (vm.exceptionGroups.length !== vm.stats.number_of_exception_groups) { + vm.stats.number_of_exception_groups = vm.exceptionGroups.length; + notifier.notify('ExceptionGroupCountUpdated', vm.stats.number_of_exception_groups); + } + } return true; }); }; - var localtimeout; - var startTimer = function (time) { - time = time || 5000; - localtimeout = $timeout(function () { - vm.loadingData = true; + var historicGroupsEtag; - autoGetExceptionGroups().then(function (result) { - vm.loadingData = false; + var getHistoricGroups = function() { + serviceControlService.getHistoricGroups() + .then(function (response) { + if (historicGroupsEtag === response.etag) { + return true; + } + historicGroupsEtag = response.etag; + vm.historicGroups = response.data.historic_operations; }); - }, time); + }; + + function getMessageForRetryStatus(retryStatus, failed) { + if (retryStatus === 'waiting') { + return 'Retry request initiated...'; + } + + if (retryStatus === 'queued') { + return 'Retry request in progress. Next Step - Queued.'; + } + + if (retryStatus === 'preparing') { + return 'Retry request in progress. Step 1/2 - Preparing messages...'; + } + + if (retryStatus === 'forwarding') { + return 'Retry request in progress. Step 2/2 - Sending messages to retry...'; + } + + if (retryStatus === 'completed') { + if (failed) { + return 'ServiceControl had to restart while this operation was in progress. Not all messages were submitted for retrying.'; + } + + return 'Messages successfully submitted for retrying'; + } + + return ''; } + var groupUpdatedInterval = $interval(function () { + getHistoricGroups(); + vm.updateExceptionGroups(); + }, 5000); + $scope.$on("$destroy", function (event) { - $timeout.cancel(localtimeout); + if (angular.isDefined(groupUpdatedInterval)) { + $interval.cancel(groupUpdatedInterval); + groupUpdatedInterval = undefined; + } }); + var retryOperationEventHandler = function (data, status) { + var group = vm.exceptionGroups.filter(function (item) { return item.id === data.request_id }); + + group.forEach(function (item) { + if (status === 'preparing' && data.progress.percentage === 1) { + status = 'queued'; + } + item + .workflow_state = + createWorkflowState(status, + getMessageForRetryStatus(status, data.failed || false), + data.progress.percentage, + data.failed || false); + + item.retry_remaining_count = data.progress.messages_remaining; + item.retry_start_time = data.start_time; + + if (status === 'completed') { + item.need_user_acknowledgement = true; + item.retry_completion_time = data.completion_time; + } + }); + }; + + notifier.subscribe($scope, function (event, data) { + retryOperationEventHandler(data, 'waiting'); + }, 'RetryOperationWaiting'); + notifier.subscribe($scope, function (event, data) { - $timeout.cancel(localtimeout); - startTimer(); - }, 'MessagesSubmittedForRetry'); + retryOperationEventHandler(data, 'preparing'); + }, 'RetryOperationPreparing'); + + notifier.subscribe($scope, function (event, data) { + retryOperationEventHandler(data, 'forwarding'); + }, 'RetryOperationForwarding'); notifier.subscribe($scope, function (event, data) { - $timeout.cancel(localtimeout); - startTimer(); - }, 'FailedMessageGroupArchived'); + retryOperationEventHandler(data, 'forwarding'); + }, 'RetryOperationForwarded'); + + notifier.subscribe($scope, function (event, data) { + retryOperationEventHandler(data, 'completed'); + getHistoricGroups(); + if (data.failed) { + toastService.showInfo("Group " + data.originator + " was retried however and error have occured and not all messages were retried. Retry the remaining messages afterwards.", "Retry operation completed", true); + } else { + toastService.showInfo("Group " + data.originator + " was retried succesfully.", "Retry operation completed", true); + } + }, 'RetryOperationCompleted'); // INIT initialLoad(); + getHistoricGroups(); } controller.$inject = [ @@ -159,7 +334,8 @@ "sharedDataService", "notifyService", "serviceControlService", - "failedMessageGroupsService" + "failedMessageGroupsService", + "toastService" ]; angular.module("sc") diff --git a/src/ServicePulse.Host/app/js/views/failed_groups/view.html b/src/ServicePulse.Host/app/js/views/failed_groups/view.html index c576dedc7..14c953ed8 100644 --- a/src/ServicePulse.Host/app/js/views/failed_groups/view.html +++ b/src/ServicePulse.Host/app/js/views/failed_groups/view.html @@ -11,15 +11,55 @@

Failed Messages

- +
+ +
+ +
+
+
+
+
+
+

{{group.originator}}

+
+
- +
+
+ +
+
+
+
+
+
+ There is only 1 completed group retry + There are only {{vm.historicGroups.length}} completed group retries +
+
-
-
-
-
+
-
-
-
-
+
+ + +
+
+ + +
+
-
-
-
-

Processing Group {{group.title}}

-

It can take some time for these messages to be processed by your system. Please be patient.

-

- If you leave this screen or refresh the data in this list, messages may appear as if they have not been processed when you return. -

+
+ +
+
+
+
+
+
+

{{group.title}}

+ +
-
-
-
- -
-
-
-
-
-
-

{{group.title}}

-
+
+ + + +
+
+
+
+
+
+
    +
  • +
    Initialize retry request...
    +
  • +
  • - +
    +
    +
    Prepare messages...
    +
    - -

    +
    +
    +
    + {{group.workflow_state.total | number : 0}}% +
    +
    +
    +
    +
  • +
  • + +
    +
    +
    Send messages to retry...
    +
    +
    + (Queued) +
    +
    + +
    +
    + {{group.workflow_state.total | number : 0}}% +
    +
    +
    +
  • +
  • +
    Retry request completed
    + +
    + WARNING: Not all messages will be retried because ServiceControl had to restart. You need to request retrying the remaining messages. +
    + +
  • +
+ + +
+
+
-
-
-
- - - +
+
+
+
+

{{group.workflow_state.message}}

+
+ +
+
+
+
-
+
+
diff --git a/src/ServicePulse.Host/app/js/views/failed_messages/view.html b/src/ServicePulse.Host/app/js/views/failed_messages/view.html index f72669b16..2675d1e23 100644 --- a/src/ServicePulse.Host/app/js/views/failed_messages/view.html +++ b/src/ServicePulse.Host/app/js/views/failed_messages/view.html @@ -57,10 +57,10 @@

Failed Messages

-
+
- +
diff --git a/src/SmokeTest.Client/Program.cs b/src/SmokeTest.Client/Program.cs index 0d9637f72..bc0503f63 100644 --- a/src/SmokeTest.Client/Program.cs +++ b/src/SmokeTest.Client/Program.cs @@ -28,7 +28,7 @@ static void Main() Console.ForegroundColor = ConsoleColor.White; Console.WriteLine("-------------------------------------"); Console.WriteLine("[ A ] Send 1 good Message"); - Console.WriteLine("[ B ] Send 10 bad Messages"); + Console.WriteLine("[ B ] Send 1000 bad Messages"); Console.WriteLine("[ C ] Send Infinite bad Messages "); Console.WriteLine("[ Q ] Quit"); Console.WriteLine("-------------------------------------"); @@ -47,7 +47,7 @@ static void Main() break; case ConsoleKey.B: - for (var i = 0; i < 10; i++) + for (var i = 0; i < 1000; i++) { SendMessage(bus, true, text); }