Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add new metrics for spending plans #2932

Merged
merged 31 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b992986
feat: Add `keys()` method to ICacheClient
victor-yanev Sep 3, 2024
06c4b27
chore: fix import in localLRUCache.ts
victor-yanev Sep 3, 2024
d56c670
chore: Add new metrics for tracking spending plans
victor-yanev Sep 3, 2024
11d5836
chore: fix wrong comment
victor-yanev Sep 3, 2024
87ee810
chore: Add new metrics for tracking spending plans
victor-yanev Sep 3, 2024
16dbd3b
chore: Add new metrics for tracking spending plans
victor-yanev Sep 3, 2024
7a88d5e
fix: Add support for glob patterns (equivalent to redis functionality…
victor-yanev Sep 4, 2024
3589a1a
test: add `KEYS Test Suite` in localLRUCache.spec.ts and redisCache.s…
victor-yanev Sep 4, 2024
6899bd0
test: extend cacheService.spec.ts with tests for `keys()`
victor-yanev Sep 4, 2024
e1730ce
chore: make log messages in localLRUCache.ts and redisCache.ts consis…
victor-yanev Sep 4, 2024
3c23846
chore: fix code duplication + vulnerable regex
victor-yanev Sep 4, 2024
33a4785
chore: fix code duplication
victor-yanev Sep 4, 2024
943a26b
chore: remove redundant comment
victor-yanev Sep 4, 2024
ed04800
chore: extend coverage
victor-yanev Sep 4, 2024
d4036e6
chore: address comments + fix issues in cacheService.ts
victor-yanev Sep 5, 2024
a1dbd58
test: extend hbarLimitService.spec.ts
victor-yanev Sep 5, 2024
2d87252
chore: final touches
victor-yanev Sep 5, 2024
7d14288
chore: fix flaky tests in redisCache.spec.ts
victor-yanev Sep 5, 2024
76fb154
chore: fix flaky tests in redisCache.spec.ts
victor-yanev Sep 5, 2024
c0acbf3
Merge branch 'update-cache-client-with-method-for-keys' into add-new-…
victor-yanev Sep 5, 2024
f73b267
test: extend hbarSpendingPlanRepository.spec.ts
victor-yanev Sep 5, 2024
683507b
fix: flaky test in hbarSpendingPlanRepository.spec.ts
victor-yanev Sep 5, 2024
086a452
Merge branch 'main' into add-new-metrics-for-spending-plans
victor-yanev Sep 5, 2024
d4ea964
Merge branch 'main' into add-new-metrics-for-spending-plans
victor-yanev Sep 9, 2024
540bb27
fix: remove unused constant in hbarLimitService.spec.ts
victor-yanev Sep 9, 2024
6b16714
Merge branch 'main' into add-new-metrics-for-spending-plans
victor-yanev Sep 9, 2024
35e54ea
chore: address comments
victor-yanev Sep 10, 2024
d33d822
Merge remote-tracking branch 'origin/add-new-metrics-for-spending-pla…
victor-yanev Sep 10, 2024
2bdb303
Merge branch 'main' into add-new-metrics-for-spending-plans
victor-yanev Sep 10, 2024
5a3e361
Merge branch 'main' into add-new-metrics-for-spending-plans
quiet-node Sep 10, 2024
44cc5b6
Merge branch 'main' into add-new-metrics-for-spending-plans
ebadiere Sep 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,30 @@ export class HbarSpendingPlanRepository {
}
}

/**
* Finds all active hbar spending plans by subscription type.
* @param {SubscriptionType} subscriptionType - The subscription type to filter by.
* @returns {Promise<IDetailedHbarSpendingPlan[]>} - A promise that resolves with the active spending plans.
*/
async findAllActiveBySubscriptionType(subscriptionType: SubscriptionType): Promise<IDetailedHbarSpendingPlan[]> {
const callerMethod = this.findAllActiveBySubscriptionType.name;
const keys = await this.cache.keys(`${this.collectionKey}:*`, callerMethod);
victor-yanev marked this conversation as resolved.
Show resolved Hide resolved
const plans = await Promise.all(keys.map((key) => this.cache.getAsync<IHbarSpendingPlan>(key, callerMethod)));
return Promise.all(
plans
.filter((plan) => plan.subscriptionType === subscriptionType && plan.active)
.map(
async (plan) =>
quiet-node marked this conversation as resolved.
Show resolved Hide resolved
new HbarSpendingPlan({
...plan,
createdAt: new Date(plan.createdAt),
spentToday: await this.getSpentToday(plan.id),
spendingHistory: await this.getSpendingHistory(plan.id),
}),
),
);
}

/**
* Gets the cache key for an {@link IHbarSpendingPlan}.
* @param id - The ID of the plan to get the key for.
Expand Down
66 changes: 66 additions & 0 deletions packages/relay/src/lib/services/hbarLimitService/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ export class HbarLimitService implements IHbarLimitService {
*/
private readonly hbarLimitRemainingGauge: Gauge;

/**
* Tracks the number of unique spending plans that have been utilized on a daily basis
* (i.e., plans that had expenses added to them).
*
* For basic spending plans, this equates to the number of unique users who have made requests on that day,
* since each user has their own individual spending plan.
*
* @private
*/
private readonly dailyUniqueSpendingPlansCounter: Record<SubscriptionType, Counter>;

/**
* Tracks the average daily spending plan usages.
* @private
*/
private readonly averageDailySpendingPlanUsagesGauge: Record<SubscriptionType, Gauge>;

/**
* The remaining budget for the rate limiter.
* @private
Expand Down Expand Up @@ -86,6 +103,35 @@ export class HbarLimitService implements IHbarLimitService {
});
this.hbarLimitRemainingGauge.set(this.totalBudget);
this.remainingBudget = this.totalBudget;

this.dailyUniqueSpendingPlansCounter = Object.values(SubscriptionType).reduce(
(acc, type) => {
const dailyUniqueSpendingPlansCounterName = `daily_unique_spending_plans_counter_${type.toLowerCase()}`;
this.register.removeSingleMetric(dailyUniqueSpendingPlansCounterName);
acc[type] = new Counter({
name: dailyUniqueSpendingPlansCounterName,
help: `Tracks the number of unique spending plans used daily for ${type} subscription type`,
registers: [register],
});
return acc;
},
{} as Record<SubscriptionType, Counter>,
);

this.averageDailySpendingPlanUsagesGauge = Object.values(SubscriptionType).reduce(
(acc, type) => {
const averageDailySpendingGaugeName = `average_daily_spending_plan_usages_gauge_${type.toLowerCase()}`;
this.register.removeSingleMetric(averageDailySpendingGaugeName);
acc[type] = new Gauge({
name: averageDailySpendingGaugeName,
help: `Tracks the average daily spending plan usages for ${type} subscription type`,
registers: [register],
});
return acc;
},
{} as Record<SubscriptionType, Gauge>,
);

// Reset the rate limiter at the start of the next day
const now = Date.now();
const tomorrow = new Date(now + HbarLimitService.ONE_DAY_IN_MILLIS);
Expand Down Expand Up @@ -168,11 +214,19 @@ export class HbarLimitService implements IHbarLimitService {
}`,
);

// Check if the spending plan is being used for the first time today
if (spendingPlan.spentToday === 0) {
this.dailyUniqueSpendingPlansCounter[spendingPlan.subscriptionType].inc(1);
}

await this.hbarSpendingPlanRepository.addAmountToSpentToday(spendingPlan.id, cost);
await this.hbarSpendingPlanRepository.addAmountToSpendingHistory(spendingPlan.id, cost);
this.remainingBudget -= cost;
this.hbarLimitRemainingGauge.set(this.remainingBudget);

// Done asynchronously in the background
this.updateAverageDailyUsagePerSubscriptionType(spendingPlan.subscriptionType).then();
victor-yanev marked this conversation as resolved.
Show resolved Hide resolved

this.logger.trace(
`${requestIdPrefix} HBAR rate limit expense update: cost=${cost}, remainingBudget=${this.remainingBudget}`,
);
Expand Down Expand Up @@ -204,6 +258,18 @@ export class HbarLimitService implements IHbarLimitService {
return false;
}

/**
* Updates the average daily usage per subscription type.
* @param {SubscriptionType} subscriptionType - The subscription type to update the average daily usage for.
* @private {Promise<void>} - A promise that resolves when the average daily usage has been updated.
*/
private async updateAverageDailyUsagePerSubscriptionType(subscriptionType: SubscriptionType): Promise<void> {
const plans = await this.hbarSpendingPlanRepository.findAllActiveBySubscriptionType(subscriptionType);
const totalUsage = plans.reduce((total, plan) => total + plan.spentToday, 0);
const averageUsage = Math.round(totalUsage / plans.length);
this.averageDailySpendingPlanUsagesGauge[subscriptionType].set(averageUsage);
}

/**
* Checks if the rate limiter should be reset.
* @returns {boolean} - `true` if the rate limiter should be reset, otherwise `false`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('HbarSpendingPlanRepository', function () {
let redisUrl: string | undefined;
let redisInMemoryServer: RedisInMemoryServer;

this.beforeAll(async () => {
before(async () => {
redisInMemoryServer = new RedisInMemoryServer(logger.child({ name: `in-memory redis server` }), 6380);
await redisInMemoryServer.start();
test = process.env.TEST;
Expand All @@ -62,7 +62,7 @@ describe('HbarSpendingPlanRepository', function () {
repository = new HbarSpendingPlanRepository(cacheService, logger.child({ name: `HbarSpendingPlanRepository` }));
});

this.afterAll(async () => {
after(async () => {
await cacheService.disconnectRedisClient();
await redisInMemoryServer.stop();
process.env.TEST = test;
Expand All @@ -78,6 +78,10 @@ describe('HbarSpendingPlanRepository', function () {
});
}

afterEach(async () => {
await cacheService.clear();
});

describe('create', () => {
it('creates a plan successfully', async () => {
const subscriptionType = SubscriptionType.BASIC;
Expand Down Expand Up @@ -199,14 +203,15 @@ describe('HbarSpendingPlanRepository', function () {
});

describe('getSpentToday', () => {
const mockedOneDayInMillis: number = 200;
let oneDayInMillis: number;

beforeEach(() => {
// save the oneDayInMillis value
oneDayInMillis = repository['oneDayInMillis'];
// set oneDayInMillis to 1 second for testing
// @ts-ignore
repository['oneDayInMillis'] = 1000;
repository['oneDayInMillis'] = mockedOneDayInMillis;
});

afterEach(() => {
Expand Down Expand Up @@ -242,7 +247,7 @@ describe('HbarSpendingPlanRepository', function () {
await repository.addAmountToSpentToday(createdPlan.id, amount);
await expect(repository.getSpentToday(createdPlan.id)).to.eventually.equal(amount);

await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, mockedOneDayInMillis + 100));

await expect(repository.getSpentToday(createdPlan.id)).to.eventually.equal(0);
});
Expand Down Expand Up @@ -318,6 +323,55 @@ describe('HbarSpendingPlanRepository', function () {
);
});
});

describe('findAllActiveBySubscriptionType', () => {
it('returns an empty array if no active plans exist for the subscription type', async () => {
const subscriptionType = SubscriptionType.BASIC;
const activePlans = await repository.findAllActiveBySubscriptionType(subscriptionType);
expect(activePlans).to.deep.equal([]);
});

it('returns all active plans for the subscription type', async () => {
const subscriptionType = SubscriptionType.BASIC;
const createdPlan1 = await repository.create(subscriptionType);
const createdPlan2 = await repository.create(subscriptionType);

const activePlans = await repository.findAllActiveBySubscriptionType(subscriptionType);
expect(activePlans).to.have.lengthOf(2);
expect(activePlans.map((plan) => plan.id)).to.include.members([createdPlan1.id, createdPlan2.id]);
});

it('does not return inactive plans for the subscription type', async () => {
const subscriptionType = SubscriptionType.BASIC;
const activePlan = await repository.create(subscriptionType);
const inactivePlan = await repository.create(subscriptionType);

// Manually set the plan to inactive
const key = `${repository['collectionKey']}:${inactivePlan.id}`;
await cacheService.set(key, { ...inactivePlan, active: false }, 'test');

const activePlans = await repository.findAllActiveBySubscriptionType(subscriptionType);
expect(activePlans).to.deep.equal([activePlan]);
});

it('returns only active plans for the specified subscription type', async () => {
const basicPlan = await repository.create(SubscriptionType.BASIC);
const extendedPlan = await repository.create(SubscriptionType.EXTENDED);
const privilegedPlan = await repository.create(SubscriptionType.PRIVILEGED);

const activeBasicPlans = await repository.findAllActiveBySubscriptionType(SubscriptionType.BASIC);
expect(activeBasicPlans).to.have.lengthOf(1);
expect(activeBasicPlans[0].id).to.equal(basicPlan.id);

const activeExtendedPlans = await repository.findAllActiveBySubscriptionType(SubscriptionType.EXTENDED);
expect(activeExtendedPlans).to.have.lengthOf(1);
expect(activeExtendedPlans[0].id).to.equal(extendedPlan.id);

const activePrivilegedPlans = await repository.findAllActiveBySubscriptionType(SubscriptionType.PRIVILEGED);
expect(activePrivilegedPlans).to.have.lengthOf(1);
expect(activePrivilegedPlans[0].id).to.equal(privilegedPlan.id);
});
});
};

describe('with shared cache', () => {
Expand Down
Loading
Loading