1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

chore: resource limits service (#10709)

https://linear.app/unleash/issue/2-3927/implement-resource-limits-service

Implements a resource limits service.

The implementation looks trivial (or even redundant) in OSS, but by
implementing a resource limits service we can make this potentially
dynamic and overridable.
This commit is contained in:
Nuno Góis 2025-10-01 09:57:18 +01:00 committed by GitHub
parent 9d996f14d9
commit 7462465a0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 100 additions and 39 deletions

View File

@ -9,6 +9,7 @@ import {
} from '../events/createEventsService.js';
import FakeApiTokenStore from '../../../test/fixtures/fake-api-token-store.js';
import { ApiTokenStore } from '../../db/api-token-store.js';
import { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
export const createApiTokenService = (
db: Db,
@ -23,11 +24,13 @@ export const createApiTokenService = (
);
const environmentStore = new EnvironmentStore(db, eventBus, config);
const eventService = createEventsService(db, config);
const resourceLimitsService = new ResourceLimitsService(config);
return new ApiTokenService(
{ apiTokenStore, environmentStore },
config,
eventService,
resourceLimitsService,
);
};
@ -36,23 +39,27 @@ export const createFakeApiTokenService = (
): {
apiTokenService: ApiTokenService;
eventService: EventService;
resourceLimitsService: ResourceLimitsService;
apiTokenStore: FakeApiTokenStore;
environmentStore: IEnvironmentStore;
} => {
const apiTokenStore = new FakeApiTokenStore();
const environmentStore = new FakeEnvironmentStore();
const eventService = createFakeEventsService(config);
const resourceLimitsService = new ResourceLimitsService(config);
const apiTokenService = new ApiTokenService(
{ apiTokenStore, environmentStore },
config,
eventService,
resourceLimitsService,
);
return {
apiTokenService,
apiTokenStore,
eventService,
resourceLimitsService,
environmentStore,
};
};

View File

@ -65,12 +65,13 @@ import {
createFakeFeatureLinkService,
createFeatureLinkService,
} from '../feature-links/createFeatureLinkService.js';
import { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
export const createFeatureToggleService = (
db: Db,
config: IUnleashConfig,
): FeatureToggleService => {
const { getLogger, eventBus, flagResolver, resourceLimits } = config;
const { getLogger, eventBus, flagResolver } = config;
const featureStrategiesStore = new FeatureStrategiesStore(
db,
eventBus,
@ -138,6 +139,8 @@ export const createFeatureToggleService = (
const featureLinkService = createFeatureLinkService(config)(db);
const resourceLimitsService = new ResourceLimitsService(config);
const featureToggleService = new FeatureToggleService(
{
featureStrategiesStore,
@ -149,7 +152,7 @@ export const createFeatureToggleService = (
contextFieldStore,
strategyStore,
},
{ getLogger, flagResolver, eventBus, resourceLimits },
{ getLogger, flagResolver, eventBus },
{
segmentService,
accessService,
@ -162,13 +165,14 @@ export const createFeatureToggleService = (
featureCollaboratorsReadModel,
featureLinksReadModel,
featureLinkService,
resourceLimitsService,
},
);
return featureToggleService;
};
export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
const { getLogger, flagResolver, resourceLimits } = config;
const { getLogger, flagResolver } = config;
const eventStore = new FakeEventStore();
const strategyStore = new FakeStrategiesStore();
const featureStrategiesStore = new FakeFeatureStrategiesStore();
@ -206,6 +210,8 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
const featureLinksReadModel = new FakeFeatureLinksReadModel();
const { featureLinkService } = createFakeFeatureLinkService(config);
const resourceLimitsService = new ResourceLimitsService(config);
const featureToggleService = new FeatureToggleService(
{
featureStrategiesStore,
@ -221,7 +227,6 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
getLogger,
flagResolver,
eventBus: new EventEmitter(),
resourceLimits,
},
{
segmentService,
@ -235,6 +240,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
featureCollaboratorsReadModel,
featureLinksReadModel,
featureLinkService,
resourceLimitsService,
},
);
return {

View File

@ -114,9 +114,9 @@ import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-li
import { throwExceedsLimitError } from '../../error/exceeds-limit-error.js';
import type { Collaborator } from './types/feature-collaborators-read-model-type.js';
import { sortStrategies } from '../../util/sortStrategies.js';
import type { ResourceLimitsSchema } from '../../openapi/index.js';
import type FeatureLinkService from '../feature-links/feature-link-service.js';
import type { IFeatureLink } from '../feature-links/feature-links-read-model-type.js';
import type { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
interface IFeatureContext {
featureName: string;
projectId: string;
@ -161,7 +161,7 @@ export type Stores = Pick<
export type Config = Pick<
IUnleashConfig,
'getLogger' | 'flagResolver' | 'eventBus' | 'resourceLimits'
'getLogger' | 'flagResolver' | 'eventBus'
>;
export type ServicesAndReadModels = {
@ -176,6 +176,7 @@ export type ServicesAndReadModels = {
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
featureLinkService: FeatureLinkService;
featureLinksReadModel: IFeatureLinksReadModel;
resourceLimitsService: ResourceLimitsService;
};
export class FeatureToggleService {
@ -223,7 +224,7 @@ export class FeatureToggleService {
private eventBus: EventEmitter;
private resourceLimits: ResourceLimitsSchema;
private resourceLimitsService: ResourceLimitsService;
constructor(
{
@ -236,7 +237,7 @@ export class FeatureToggleService {
contextFieldStore,
strategyStore,
}: Stores,
{ getLogger, flagResolver, eventBus, resourceLimits }: Config,
{ getLogger, flagResolver, eventBus }: Config,
{
segmentService,
accessService,
@ -249,6 +250,7 @@ export class FeatureToggleService {
featureCollaboratorsReadModel,
featureLinksReadModel,
featureLinkService,
resourceLimitsService,
}: ServicesAndReadModels,
) {
this.logger = getLogger('services/feature-toggle-service.ts');
@ -273,7 +275,7 @@ export class FeatureToggleService {
this.featureLinksReadModel = featureLinksReadModel;
this.featureLinkService = featureLinkService;
this.eventBus = eventBus;
this.resourceLimits = resourceLimits;
this.resourceLimitsService = resourceLimitsService;
}
async validateFeaturesContext(
@ -396,7 +398,8 @@ export class FeatureToggleService {
environment: string;
featureName: string;
}) {
const limit = this.resourceLimits.featureEnvironmentStrategies;
const { featureEnvironmentStrategies: limit } =
await this.resourceLimitsService.getResourceLimits();
const existingCount = (
await this.featureStrategiesStore.getStrategiesForFeatureEnv(
featureEnv.projectId,
@ -412,14 +415,14 @@ export class FeatureToggleService {
}
}
private validateConstraintsLimit(constraints: {
private async validateConstraintsLimit(constraints: {
updated: IConstraint[];
existing: IConstraint[];
}) {
const {
constraints: constraintsLimit,
constraintValues: constraintValuesLimit,
} = this.resourceLimits;
} = await this.resourceLimitsService.getResourceLimits();
if (
constraints.updated.length > constraintsLimit &&
@ -711,7 +714,7 @@ export class FeatureToggleService {
const { name, title, disabled, sortOrder } = strategyConfig;
let { constraints, parameters, variants } = strategyConfig;
if (constraints && constraints.length > 0) {
this.validateConstraintsLimit({
await this.validateConstraintsLimit({
updated: constraints,
existing: existing?.constraints ?? [],
});
@ -1264,7 +1267,8 @@ export class FeatureToggleService {
private async validateFeatureFlagLimit() {
const currentFlagCount = await this.featureToggleStore.count();
const limit = this.resourceLimits.featureFlags;
const { featureFlags: limit } =
await this.resourceLimitsService.getResourceLimits();
if (currentFlagCount >= limit) {
throwExceedsLimitError(this.eventBus, {
resource: 'feature flag',

View File

@ -9,6 +9,7 @@ import {
FavoritesService,
GroupService,
ProjectService,
ResourceLimitsService,
} from '../../services/index.js';
import FakeGroupStore from '../../../test/fixtures/fake-group-store.js';
import FakeEventStore from '../../../test/fixtures/fake-event-store.js';
@ -117,10 +118,13 @@ export const createProjectService = (
const privateProjectChecker = createPrivateProjectChecker(db, config);
const resourceLimitsService = new ResourceLimitsService(config);
const apiTokenService = new ApiTokenService(
{ apiTokenStore, environmentStore },
config,
eventService,
resourceLimitsService,
);
const projectReadModel = createProjectReadModel(
@ -153,6 +157,7 @@ export const createProjectService = (
eventService,
privateProjectChecker,
apiTokenService,
resourceLimitsService,
);
};
@ -189,10 +194,13 @@ export const createFakeProjectService = (config: IUnleashConfig) => {
eventService,
);
const resourceLimitsService = new ResourceLimitsService(config);
const apiTokenService = new ApiTokenService(
{ apiTokenStore, environmentStore },
config,
eventService,
resourceLimitsService,
);
const projectReadModel = createFakeProjectReadModel();
@ -221,6 +229,7 @@ export const createFakeProjectService = (config: IUnleashConfig) => {
eventService,
privateProjectChecker,
apiTokenService,
resourceLimitsService,
);
return {
projectService,

View File

@ -67,10 +67,7 @@ import { calculateAverageTimeToProd } from '../feature-toggle/time-to-production
import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type.js';
import { uniqueByKey } from '../../util/unique.js';
import { BadDataError, PermissionError } from '../../error/index.js';
import type {
ProjectDoraMetricsSchema,
ResourceLimitsSchema,
} from '../../openapi/index.js';
import type { ProjectDoraMetricsSchema } from '../../openapi/index.js';
import { checkFeatureNamingData } from '../feature-naming-pattern/feature-naming-validation.js';
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType.js';
import type EventService from '../events/event-service.js';
@ -89,6 +86,7 @@ import { canGrantProjectRole } from './can-grant-project-role.js';
import { batchExecute } from '../../util/index.js';
import metricsHelper from '../../util/metrics-helper.js';
import { FUNCTION_TIME } from '../../metric-events.js';
import type { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
type Days = number;
type Count = number;
@ -148,7 +146,7 @@ export default class ProjectService {
private isEnterprise: boolean;
private resourceLimits: ResourceLimitsSchema;
private resourceLimitsService: ResourceLimitsService;
private eventBus: EventEmitter;
@ -193,6 +191,7 @@ export default class ProjectService {
eventService: EventService,
privateProjectChecker: IPrivateProjectChecker,
apiTokenService: ApiTokenService,
resourceLimitsService: ResourceLimitsService,
) {
this.projectStore = projectStore;
this.projectOwnersReadModel = projectOwnersReadModel;
@ -213,7 +212,7 @@ export default class ProjectService {
this.logger = config.getLogger('services/project-service.js');
this.flagResolver = config.flagResolver;
this.isEnterprise = config.isEnterprise;
this.resourceLimits = config.resourceLimits;
this.resourceLimitsService = resourceLimitsService;
this.eventBus = config.eventBus;
this.projectReadModel = projectReadModel;
this.onboardingReadModel = onboardingReadModel;
@ -318,7 +317,9 @@ export default class ProjectService {
}
async validateProjectLimit() {
const limit = Math.max(this.resourceLimits.projects, 1);
const { projects } =
await this.resourceLimitsService.getResourceLimits();
const limit = Math.max(projects, 1);
const projectCount = await this.projectStore.count();
if (projectCount >= limit) {

View File

@ -0,0 +1,14 @@
import type { IUnleashConfig } from '../../types/option.js';
import type { ResourceLimitsSchema } from '../../openapi/index.js';
export class ResourceLimitsService {
private config: IUnleashConfig;
constructor(config: IUnleashConfig) {
this.config = config;
}
async getResourceLimits(): Promise<ResourceLimitsSchema> {
return this.config.resourceLimits;
}
}

View File

@ -1,5 +1,5 @@
import type { Db, IUnleashConfig } from '../../types/index.js';
import { SegmentService } from '../../services/index.js';
import { ResourceLimitsService, SegmentService } from '../../services/index.js';
import type { ISegmentService } from './segment-service-interface.js';
import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store.js';
import SegmentStore from './segment-store.js';
@ -51,6 +51,8 @@ export const createSegmentService = (
const eventService = createEventsService(db, config);
const resourceLimitsService = new ResourceLimitsService(config);
return new SegmentService(
{ segmentStore, featureStrategiesStore },
changeRequestAccessReadModel,
@ -58,6 +60,7 @@ export const createSegmentService = (
config,
eventService,
privateProjectChecker,
resourceLimitsService,
);
};
@ -74,6 +77,8 @@ export const createFakeSegmentService = (
const eventService = createFakeEventsService(config);
const resourceLimitsService = new ResourceLimitsService(config);
return new SegmentService(
{ segmentStore, featureStrategiesStore },
changeRequestAccessReadModel,
@ -81,5 +86,6 @@ export const createFakeSegmentService = (
config,
eventService,
privateProjectChecker,
resourceLimitsService,
);
};

View File

@ -25,11 +25,9 @@ import type { IChangeRequestAccessReadModel } from '../change-request-access-ser
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType.js';
import type EventService from '../events/event-service.js';
import type { IChangeRequestSegmentUsageReadModel } from '../change-request-segment-usage-service/change-request-segment-usage-read-model.js';
import type {
ResourceLimitsSchema,
UpsertSegmentSchema,
} from '../../openapi/index.js';
import type { UpsertSegmentSchema } from '../../openapi/index.js';
import { throwExceedsLimitError } from '../../error/exceeds-limit-error.js';
import type { ResourceLimitsService } from '../resource-limits/resource-limits-service.js';
export class SegmentService implements ISegmentService {
private logger: Logger;
@ -50,7 +48,7 @@ export class SegmentService implements ISegmentService {
private privateProjectChecker: IPrivateProjectChecker;
private resourceLimits: ResourceLimitsSchema;
private resourceLimitsService: ResourceLimitsService;
constructor(
{
@ -62,6 +60,7 @@ export class SegmentService implements ISegmentService {
config: IUnleashConfig,
eventService: EventService,
privateProjectChecker: IPrivateProjectChecker,
resourceLimitsService: ResourceLimitsService,
) {
this.segmentStore = segmentStore;
this.featureStrategiesStore = featureStrategiesStore;
@ -72,7 +71,7 @@ export class SegmentService implements ISegmentService {
this.privateProjectChecker = privateProjectChecker;
this.logger = config.getLogger('services/segment-service.ts');
this.flagResolver = config.flagResolver;
this.resourceLimits = config.resourceLimits;
this.resourceLimitsService = resourceLimitsService;
this.config = config;
}
@ -136,7 +135,8 @@ export class SegmentService implements ISegmentService {
}
async validateSegmentLimit() {
const limit = this.resourceLimits.segments;
const { segments: limit } =
await this.resourceLimitsService.getResourceLimits();
const segmentCount = await this.segmentStore.count();

View File

@ -31,9 +31,9 @@ import type EventService from '../features/events/event-service.js';
import { addMinutes, isPast } from 'date-fns';
import metricsHelper from '../util/metrics-helper.js';
import { FUNCTION_TIME } from '../metric-events.js';
import type { ResourceLimitsSchema } from '../openapi/index.js';
import { throwExceedsLimitError } from '../error/exceeds-limit-error.js';
import type EventEmitter from 'events';
import type { ResourceLimitsService } from '../features/resource-limits/resource-limits-service.js';
const resolveTokenPermissions = (tokenType: string) => {
if (tokenType === ApiTokenType.ADMIN) {
@ -73,7 +73,7 @@ export class ApiTokenService {
private timer: Function;
private resourceLimits: ResourceLimitsSchema;
private resourceLimitsService: ResourceLimitsService;
private eventBus: EventEmitter;
@ -84,20 +84,17 @@ export class ApiTokenService {
}: Pick<IUnleashStores, 'apiTokenStore' | 'environmentStore'>,
config: Pick<
IUnleashConfig,
| 'getLogger'
| 'authentication'
| 'flagResolver'
| 'eventBus'
| 'resourceLimits'
'getLogger' | 'authentication' | 'flagResolver' | 'eventBus'
>,
eventService: EventService,
resourceLimitsService: ResourceLimitsService,
) {
this.store = apiTokenStore;
this.eventService = eventService;
this.resourceLimitsService = resourceLimitsService;
this.environmentStore = environmentStore;
this.flagResolver = config.flagResolver;
this.logger = config.getLogger('/services/api-token-service.ts');
this.resourceLimits = config.resourceLimits;
if (!this.flagResolver.isEnabled('useMemoizedActiveTokens')) {
// This is probably not needed because the scheduler will run it
this.fetchActiveTokens();
@ -321,7 +318,8 @@ export class ApiTokenService {
private async validateApiTokenLimit() {
const currentTokenCount = await this.store.count();
const limit = this.resourceLimits.apiTokens;
const { apiTokens: limit } =
await this.resourceLimitsService.getResourceLimits();
if (currentTokenCount >= limit) {
throwExceedsLimitError(this.eventBus, {
resource: 'api token',

View File

@ -171,6 +171,7 @@ import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-f
import type FeatureLinkService from '../features/feature-links/feature-link-service.js';
import { createUserService } from '../features/users/createUserService.js';
import { UiConfigService } from '../ui-config/ui-config-service.js';
import { ResourceLimitsService } from '../features/resource-limits/resource-limits-service.js';
export const createServices = (
stores: IUnleashStores,
@ -205,6 +206,8 @@ export const createServices = (
const unknownFlagsService = new UnknownFlagsService(stores, config);
const resourceLimitsService = new ResourceLimitsService(config);
// Initialize custom metrics service
const customMetricsService = new CustomMetricsService(config);
@ -283,6 +286,7 @@ export const createServices = (
config,
eventService,
privateProjectChecker,
resourceLimitsService,
);
const clientInstanceService = new ClientInstanceService(
@ -449,6 +453,7 @@ export const createServices = (
frontendApiService,
maintenanceService,
sessionService,
resourceLimitsService,
});
return {
@ -525,6 +530,7 @@ export const createServices = (
featureLinkService,
unknownFlagsService,
uiConfigService,
resourceLimitsService,
};
};
@ -581,6 +587,8 @@ export {
UniqueConnectionService,
FeatureLifecycleReadModel,
UnknownFlagsService,
UiConfigService,
ResourceLimitsService,
};
export interface IUnleashServices {
@ -657,4 +665,5 @@ export interface IUnleashServices {
featureLinkService: FeatureLinkService;
unknownFlagsService: UnknownFlagsService;
uiConfigService: UiConfigService;
resourceLimitsService: ResourceLimitsService;
}

View File

@ -17,6 +17,7 @@ import {
simpleAuthSettingsKey,
} from '../types/settings/simple-auth-settings.js';
import version from '../util/version.js';
import type { ResourceLimitsService } from '../features/resource-limits/resource-limits-service.js';
export class UiConfigService {
private config: IUnleashConfig;
@ -33,6 +34,8 @@ export class UiConfigService {
private maintenanceService: MaintenanceService;
private resourceLimitsService: ResourceLimitsService;
private flagResolver: IFlagResolver;
constructor(
@ -44,6 +47,7 @@ export class UiConfigService {
frontendApiService,
maintenanceService,
sessionService,
resourceLimitsService,
}: Pick<
IUnleashServices,
| 'versionService'
@ -52,6 +56,7 @@ export class UiConfigService {
| 'frontendApiService'
| 'maintenanceService'
| 'sessionService'
| 'resourceLimitsService'
>,
) {
this.config = config;
@ -62,6 +67,7 @@ export class UiConfigService {
this.frontendApiService = frontendApiService;
this.maintenanceService = maintenanceService;
this.sessionService = sessionService;
this.resourceLimitsService = resourceLimitsService;
}
async getMaxSessionsCount(): Promise<number> {
@ -114,7 +120,8 @@ export class UiConfigService {
frontendApiOrigins: frontendSettings.frontendApiOrigins,
versionInfo: await this.versionService.getVersionInfo(),
prometheusAPIAvailable: this.config.prometheusApi !== undefined,
resourceLimits: this.config.resourceLimits,
resourceLimits:
await this.resourceLimitsService.getResourceLimits(),
disablePasswordAuth,
maintenanceMode,
feedbackUriPath: this.config.feedbackUriPath,