Merge pull request #31805 from kaidubauskas-dd/kaidd/slack-module-rate-limit
Notification slack processor: make rate limit configurable
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend-module-slack': patch
|
||||
---
|
||||
|
||||
Slack notification handler throttling can now be configured with the `concurrencyLimit` and `throttleInterval` options.
|
||||
@@ -149,6 +149,8 @@ notifications:
|
||||
broadcastChannels: # Optional, if you wish to support broadcast notifications.
|
||||
- C12345678
|
||||
username: 'Backstage Bot' # Optional, defaults to the name of the Slack App.
|
||||
concurrencyLimit: 20 # Optional, number of messages allowed per interval. Defaults to 10.
|
||||
throttleInterval: 1m # Optional, Accepts ISO-8601 duration, ms-style ("1m", "30s"), or HumanDuration ({ minutes: 2 }). Defaults to 1 minute
|
||||
```
|
||||
|
||||
Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
|
||||
export interface Config {
|
||||
notifications?: {
|
||||
processors?: {
|
||||
@@ -28,6 +30,14 @@ export interface Config {
|
||||
* Names, or Slack Channel IDs. Any valid identifier that chat.postMessage can accept.
|
||||
*/
|
||||
broadcastChannels?: string[];
|
||||
/**
|
||||
* Concurrency limit for Slack notifications per backend instance of the notifications plugin, defaults to 10.
|
||||
*/
|
||||
concurrencyLimit?: number;
|
||||
/**
|
||||
* Throttle duration between Slack notifications per backend instance of the notifications plugin, defaults to 1 minute.
|
||||
*/
|
||||
throttleInterval?: HumanDuration | string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,6 +19,22 @@ import { SlackNotificationProcessor } from './SlackNotificationProcessor';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
import { WebClient } from '@slack/web-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import pThrottle from 'p-throttle';
|
||||
import { durationToMilliseconds } from '@backstage/types';
|
||||
|
||||
const throttleConfigs: Array<{ limit: number; interval: number }> = [];
|
||||
|
||||
jest.mock('p-throttle', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn((config: { limit: number; interval: number }) => {
|
||||
throttleConfigs.push(config);
|
||||
return <T extends (...args: any[]) => Promise<any> | any>(fn: T) =>
|
||||
(...args: Parameters<T>) =>
|
||||
Promise.resolve(fn(...args));
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockedPThrottle = pThrottle as jest.MockedFunction<typeof pThrottle>;
|
||||
|
||||
jest.mock('@slack/web-api', () => {
|
||||
const mockSlack = {
|
||||
@@ -128,6 +144,8 @@ describe('SlackNotificationProcessor', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
throttleConfigs.length = 0;
|
||||
mockedPThrottle.mockClear();
|
||||
});
|
||||
|
||||
it('should send a notification to a group', async () => {
|
||||
@@ -953,4 +971,81 @@ describe('SlackNotificationProcessor', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when throttling is not configured', () => {
|
||||
it('should use default concurrency limit of 10 per minute', async () => {
|
||||
const slack = new WebClient();
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(config, {
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
})[0];
|
||||
|
||||
await processor.processOptions({
|
||||
recipients: { type: 'entity', entityRef: 'group:default/mock' },
|
||||
payload: { title: 'notification' },
|
||||
});
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalled();
|
||||
expect(throttleConfigs).toEqual([
|
||||
{
|
||||
limit: 10,
|
||||
interval: durationToMilliseconds({ minutes: 1 }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when throttling is configured', () => {
|
||||
it('should use custom concurrency limit and interval values', async () => {
|
||||
const slack = new WebClient();
|
||||
const throttlingConfig = mockServices.rootConfig({
|
||||
data: {
|
||||
app: {
|
||||
baseUrl: 'https://example.org',
|
||||
},
|
||||
notifications: {
|
||||
processors: {
|
||||
slack: [
|
||||
{
|
||||
token: 'mock-token',
|
||||
concurrencyLimit: 5,
|
||||
throttleInterval: 'PT30S',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const processor = SlackNotificationProcessor.fromConfig(
|
||||
throttlingConfig,
|
||||
{
|
||||
auth,
|
||||
logger,
|
||||
catalog: catalogServiceMock({
|
||||
entities: DEFAULT_ENTITIES_RESPONSE.items,
|
||||
}),
|
||||
slack,
|
||||
},
|
||||
)[0];
|
||||
|
||||
await processor.processOptions({
|
||||
recipients: { type: 'entity', entityRef: 'group:default/mock' },
|
||||
payload: { title: 'notification' },
|
||||
});
|
||||
|
||||
expect(slack.chat.postMessage).toHaveBeenCalled();
|
||||
expect(throttleConfigs).toEqual([
|
||||
{
|
||||
limit: 5,
|
||||
interval: durationToMilliseconds({ seconds: 30 }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
parseEntityRef,
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Config, readDurationFromConfig } from '@backstage/config';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
import {
|
||||
@@ -50,6 +50,8 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
private readonly broadcastChannels?: string[];
|
||||
private readonly entityLoader: DataLoader<string, Entity | undefined>;
|
||||
private readonly username?: string;
|
||||
private readonly concurrencyLimit: number;
|
||||
private readonly throttleInterval: number;
|
||||
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
@@ -68,10 +70,18 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
const slack = options.slack ?? new WebClient(token);
|
||||
const broadcastChannels = c.getOptionalStringArray('broadcastChannels');
|
||||
const username = c.getOptionalString('username');
|
||||
const concurrencyLimit = c.getOptionalNumber('concurrencyLimit') ?? 10;
|
||||
const throttleInterval = c.has('throttleInterval')
|
||||
? durationToMilliseconds(
|
||||
readDurationFromConfig(c, { key: 'throttleInterval' }),
|
||||
)
|
||||
: durationToMilliseconds({ minutes: 1 });
|
||||
return new SlackNotificationProcessor({
|
||||
slack,
|
||||
broadcastChannels,
|
||||
username,
|
||||
concurrencyLimit,
|
||||
throttleInterval,
|
||||
...options,
|
||||
});
|
||||
});
|
||||
@@ -84,15 +94,28 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
catalog: CatalogService;
|
||||
broadcastChannels?: string[];
|
||||
username?: string;
|
||||
concurrencyLimit?: number;
|
||||
throttleInterval?: number;
|
||||
}) {
|
||||
const { auth, catalog, logger, slack, broadcastChannels, username } =
|
||||
options;
|
||||
const {
|
||||
auth,
|
||||
catalog,
|
||||
logger,
|
||||
slack,
|
||||
broadcastChannels,
|
||||
username,
|
||||
concurrencyLimit,
|
||||
throttleInterval,
|
||||
} = options;
|
||||
this.logger = logger;
|
||||
this.catalog = catalog;
|
||||
this.auth = auth;
|
||||
this.slack = slack;
|
||||
this.broadcastChannels = broadcastChannels;
|
||||
this.username = username;
|
||||
this.concurrencyLimit = concurrencyLimit ?? 10;
|
||||
this.throttleInterval =
|
||||
throttleInterval ?? durationToMilliseconds({ minutes: 1 });
|
||||
|
||||
this.entityLoader = new DataLoader<string, Entity | undefined>(
|
||||
async entityRefs => {
|
||||
@@ -134,8 +157,8 @@ export class SlackNotificationProcessor implements NotificationProcessor {
|
||||
);
|
||||
|
||||
const throttle = pThrottle({
|
||||
limit: 10,
|
||||
interval: durationToMilliseconds({ minutes: 1 }),
|
||||
limit: this.concurrencyLimit,
|
||||
interval: this.throttleInterval,
|
||||
});
|
||||
const throttled = throttle((opts: ChatPostMessageArguments) =>
|
||||
this.sendNotification(opts),
|
||||
|
||||
Reference in New Issue
Block a user