<commit_before>'use strict';

import { ApiFeaturesConfig } from '../config/gateway';
import { ApiCircuitBreakerConfig } from '../config/circuit-breaker';
import { ApiConfig } from '../config/api';
import { Stats } from '../stats/stats';
import * as express from 'express';
import * as _ from 'lodash';
import { CircuitBreaker } from './express-circuit-breaker';
import * as Groups from '../group';
import { RedisStateHandler } from './redis-state-handler';
import { Logger } from '../logger';
import { AutoWired, Inject } from 'typescript-ioc';
import { StatsRecorder } from '../stats/stats-recorder';
import { getMilisecondsInterval } from '../utils/time-intervals';
import { MiddlewareLoader } from '../utils/middleware-loader';

class StatsController {
    open: Stats;
    close: Stats;
    rejected: Stats;
}

interface BreakerInfo {
    circuitBreaker?: CircuitBreaker;
    groupValidator?: (req: express.Request, res: express.Response) => boolean;
}

@AutoWired
export class ApiCircuitBreaker {
    @Inject private logger: Logger;
    @Inject private statsRecorder: StatsRecorder;
    @Inject private middlewareLoader: MiddlewareLoader;

    private activeBreakers: Map<string, CircuitBreaker> = new Map<string, CircuitBreaker>();

    circuitBreaker(apiRouter: express.Router, api: ApiConfig, gatewayFeatures: ApiFeaturesConfig) {
        const breakerInfos: Array<BreakerInfo> = new Array<BreakerInfo>();
        const sortedBreakers = this.sortBreakers(api.circuitBreaker, api.path);
        const breakersSize = sortedBreakers.length;
        sortedBreakers.forEach((cbConfig: ApiCircuitBreakerConfig, index: number) => {
            cbConfig = this.resolveReferences(cbConfig, gatewayFeatures);
            const breakerInfo: BreakerInfo = {};
            const cbStateID = (breakersSize > 1 ? `${api.path}:${index}` : api.path);
            const cbOptions: any = {
                id: cbStateID,
                maxFailures: (cbConfig.maxFailures || 10),
                rejectMessage: (cbConfig.rejectMessage || 'Service unavailable'),
                rejectStatusCode: (cbConfig.rejectStatusCode || 503),
                stateHandler: new RedisStateHandler(cbStateID,
                                    getMilisecondsInterval(cbConfig.resetTimeout, 120000),
                                    getMilisecondsInterval(cbConfig.timeWindow)),
                timeout: getMilisecondsInterval(cbConfig.timeout, 30000),
                timeoutMessage: (cbConfig.timeoutMessage || 'Operation timeout'),
                timeoutStatusCode: (cbConfig.timeoutStatusCode || 504)
            };
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(`Configuring Circuit Breaker for path [${api.path}].`);
            }
            breakerInfo.circuitBreaker = new CircuitBreaker(cbOptions);
            this.configureCircuitBreakerEventListeners(breakerInfo, api.path, cbConfig, api.id);
            if (cbConfig.group) {
                if (this.logger.isDebugEnabled()) {
                    const groups = Groups.filter(api.group, cbConfig.group);
                    this.logger.debug(`Configuring Group filters for Circuit Breaker on path [${api.path}]. Groups [${JSON.stringify(groups)}]`);
                }
                breakerInfo.groupValidator = Groups.buildGroupAllowFilter(api.group, cbConfig.group);
            }
            breakerInfos.push(breakerInfo);
        });

        this.setupMiddlewares(apiRouter, breakerInfos);
    }

    onStateChanged(id: string, state: string) {
        const breaker: CircuitBreaker = this.activeBreakers.get(id);
        if (breaker) {
            breaker.onStateChanged(state);
        }
    }

    removeBreaker(id: string) {
        this.activeBreakers.delete(id);
    }

    removeAllBreakers() {
        this.activeBreakers.clear();
    }

    private resolveReferences(circuitBreaker: ApiCircuitBreakerConfig, features: ApiFeaturesConfig) {
        if (circuitBreaker.use && features.circuitBreaker) {
            if (features.circuitBreaker[circuitBreaker.use]) {
                circuitBreaker = _.defaults(circuitBreaker, features.circuitBreaker[circuitBreaker.use]);
            } else {
                throw new Error(`Invalid reference ${circuitBreaker.use}. There is no configuration for this id.`);
            }
        }
        return circuitBreaker;
    }

    private configureCircuitBreakerEventListeners(breakerInfo: BreakerInfo, path: string, config: ApiCircuitBreakerConfig, apiId: string) {
        const stats = this.createCircuitBreakerStats(apiId, config);
        if (stats) {
            breakerInfo.circuitBreaker.on('open', (req: express.Request) => {
                stats.open.registerOccurrence(req, 1);
            });
            breakerInfo.circuitBreaker.on('close', (req: express.Request) => {
                stats.close.registerOccurrence(req, 1);
            });
            breakerInfo.circuitBreaker.on('rejected', (req: express.Request) => {
                stats.rejected.registerOccurrence(req, 1);
            });
        }
        if (config.onOpen) {
            const openHandler = this.middlewareLoader.loadMiddleware('circuitbreaker', config.onOpen);
            breakerInfo.circuitBreaker.on('open', () => {
                openHandler(path, 'open');
            });
        }
        if (config.onClose) {
            const closeHandler = this.middlewareLoader.loadMiddleware('circuitbreaker', config.onClose);
            breakerInfo.circuitBreaker.on('close', () => {
                closeHandler(path, 'close');
            });
        }
        if (config.onRejected) {
            const rejectedHandler = this.middlewareLoader.loadMiddleware('circuitbreaker', config.onRejected);
            breakerInfo.circuitBreaker.on('rejected', () => {
                rejectedHandler(path, 'rejected');
            });
        }
    }

    private setupMiddlewares(apiRouter: express.Router, breakerInfos: Array<BreakerInfo>) {
        breakerInfos.forEach((breakerInfo: BreakerInfo) => {
            this.activeBreakers.set(breakerInfo.circuitBreaker.id, breakerInfo.circuitBreaker);
            apiRouter.use(this.buildMiddleware(breakerInfo));
        });
    }

    private buildMiddleware(breakerInfo: BreakerInfo) {
        const circuitBreakerMiddleware = breakerInfo.circuitBreaker.middleware();

        return (req: express.Request, res: express.Response, next: express.NextFunction) => {
            if (breakerInfo.groupValidator) {
                if (breakerInfo.groupValidator(req, res)) {
                    circuitBreakerMiddleware(req, res, next);
                } else {
                    next();
                }
            } else {
                circuitBreakerMiddleware(req, res, next);
            }
        };
    }

    private sortBreakers(breakers: Array<ApiCircuitBreakerConfig>, path: string): Array<ApiCircuitBreakerConfig> {
        const generalBreakers = _.filter(breakers, (value) => {
            if (value.group) {
                return true;<commit_msg>CIrcuitBreaker handlers should receive the apiId<commit_after>'use strict';

import { ApiFeaturesConfig } from '../config/gateway';
import { ApiCircuitBreakerConfig } from '../config/circuit-breaker';
import { ApiConfig } from '../config/api';
import { Stats } from '../stats/stats';
import * as express from 'express';
import * as _ from 'lodash';
import { CircuitBreaker } from './express-circuit-breaker';
import * as Groups from '../group';
import { RedisStateHandler } from './redis-state-handler';
import { Logger } from '../logger';
import { AutoWired, Inject } from 'typescript-ioc';
import { StatsRecorder } from '../stats/stats-recorder';
import { getMilisecondsInterval } from '../utils/time-intervals';
import { MiddlewareLoader } from '../utils/middleware-loader';

class StatsController {
    open: Stats;
    close: Stats;
    rejected: Stats;
}

interface BreakerInfo {
    circuitBreaker?: CircuitBreaker;
    groupValidator?: (req: express.Request, res: express.Response) => boolean;
}

@AutoWired
export class ApiCircuitBreaker {
    @Inject private logger: Logger;
    @Inject private statsRecorder: StatsRecorder;
    @Inject private middlewareLoader: MiddlewareLoader;

    private activeBreakers: Map<string, CircuitBreaker> = new Map<string, CircuitBreaker>();

    circuitBreaker(apiRouter: express.Router, api: ApiConfig, gatewayFeatures: ApiFeaturesConfig) {
        const breakerInfos: Array<BreakerInfo> = new Array<BreakerInfo>();
        const sortedBreakers = this.sortBreakers(api.circuitBreaker, api.path);
        const breakersSize = sortedBreakers.length;
        sortedBreakers.forEach((cbConfig: ApiCircuitBreakerConfig, index: number) => {
            cbConfig = this.resolveReferences(cbConfig, gatewayFeatures);
            const breakerInfo: BreakerInfo = {};
            const cbStateID = (breakersSize > 1 ? `${api.path}:${index}` : api.path);
            const cbOptions: any = {
                id: cbStateID,
                maxFailures: (cbConfig.maxFailures || 10),
                rejectMessage: (cbConfig.rejectMessage || 'Service unavailable'),
                rejectStatusCode: (cbConfig.rejectStatusCode || 503),
                stateHandler: new RedisStateHandler(cbStateID,
                                    getMilisecondsInterval(cbConfig.resetTimeout, 120000),
                                    getMilisecondsInterval(cbConfig.timeWindow)),
                timeout: getMilisecondsInterval(cbConfig.timeout, 30000),
                timeoutMessage: (cbConfig.timeoutMessage || 'Operation timeout'),
                timeoutStatusCode: (cbConfig.timeoutStatusCode || 504)
            };
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(`Configuring Circuit Breaker for path [${api.path}].`);
            }
            breakerInfo.circuitBreaker = new CircuitBreaker(cbOptions);
            this.configureCircuitBreakerEventListeners(breakerInfo, api.path, cbConfig, api.id);
            if (cbConfig.group) {
                if (this.logger.isDebugEnabled()) {
                    const groups = Groups.filter(api.group, cbConfig.group);
                    this.logger.debug(`Configuring Group filters for Circuit Breaker on path [${api.path}]. Groups [${JSON.stringify(groups)}]`);
                }
                breakerInfo.groupValidator = Groups.buildGroupAllowFilter(api.group, cbConfig.group);
            }
            breakerInfos.push(breakerInfo);
        });

        this.setupMiddlewares(apiRouter, breakerInfos);
    }

    onStateChanged(id: string, state: string) {
        const breaker: CircuitBreaker = this.activeBreakers.get(id);
        if (breaker) {
            breaker.onStateChanged(state);
        }
    }

    removeBreaker(id: string) {
        this.activeBreakers.delete(id);
    }

    removeAllBreakers() {
        this.activeBreakers.clear();
    }

    private resolveReferences(circuitBreaker: ApiCircuitBreakerConfig, features: ApiFeaturesConfig) {
        if (circuitBreaker.use && features.circuitBreaker) {
            if (features.circuitBreaker[circuitBreaker.use]) {
                circuitBreaker = _.defaults(circuitBreaker, features.circuitBreaker[circuitBreaker.use]);
            } else {
                throw new Error(`Invalid reference ${circuitBreaker.use}. There is no configuration for this id.`);
            }
        }
        return circuitBreaker;
    }

    private configureCircuitBreakerEventListeners(breakerInfo: BreakerInfo, path: string, config: ApiCircuitBreakerConfig, apiId: string) {
        const stats = this.createCircuitBreakerStats(apiId, config);
        if (stats) {
            breakerInfo.circuitBreaker.on('open', (req: express.Request) => {
                stats.open.registerOccurrence(req, 1);
            });
            breakerInfo.circuitBreaker.on('close', (req: express.Request) => {
                stats.close.registerOccurrence(req, 1);
            });
            breakerInfo.circuitBreaker.on('rejected', (req: express.Request) => {
                stats.rejected.registerOccurrence(req, 1);
            });
        }
        if (config.onOpen) {
            const openHandler = this.middlewareLoader.loadMiddleware('circuitbreaker', config.onOpen);
            breakerInfo.circuitBreaker.on('open', () => {
                openHandler(path, 'open', apiId);
            });
        }
        if (config.onClose) {
            const closeHandler = this.middlewareLoader.loadMiddleware('circuitbreaker', config.onClose);
            breakerInfo.circuitBreaker.on('close', () => {
                closeHandler(path, 'close', apiId);
            });
        }
        if (config.onRejected) {
            const rejectedHandler = this.middlewareLoader.loadMiddleware('circuitbreaker', config.onRejected);
            breakerInfo.circuitBreaker.on('rejected', () => {
                rejectedHandler(path, 'rejected', apiId);
            });
        }
    }

    private setupMiddlewares(apiRouter: express.Router, breakerInfos: Array<BreakerInfo>) {
        breakerInfos.forEach((breakerInfo: BreakerInfo) => {
            this.activeBreakers.set(breakerInfo.circuitBreaker.id, breakerInfo.circuitBreaker);
            apiRouter.use(this.buildMiddleware(breakerInfo));
        });
    }

    private buildMiddleware(breakerInfo: BreakerInfo) {
        const circuitBreakerMiddleware = breakerInfo.circuitBreaker.middleware();

        return (req: express.Request, res: express.Response, next: express.NextFunction) => {
            if (breakerInfo.groupValidator) {
                if (breakerInfo.groupValidator(req, res)) {
                    circuitBreakerMiddleware(req, res, next);
                } else {
                    next();
                }
            } else {
                circuitBreakerMiddleware(req, res, next);
            }
        };
    }

    private sortBreakers(breakers: Array<ApiCircuitBreakerConfig>, path: string): Array<ApiCircuitBreakerConfig> {
        const generalBreakers = _.filter(breakers, (value) => {
            if (value.group) {
                return true;
{'fraction_non_alphanumeric': 0.11330502476999292, 'fraction_numerical': 0.0032554847841472045, 'mean_word_length': 2.5055817415033492, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 41, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5200906', 'n_tokens_mistral': 3937, 'n_tokens_neox': 3698, 'n_words': 725}