Skip to content

Commit

Permalink
feat: Added ability to load spending plans from environment variable. (
Browse files Browse the repository at this point in the history
…#3153)

* feat: Added ability to load spening plans from environment variable.

Signed-off-by: ebadiere <[email protected]>

* feat: Now uses one property for either spending plan file or JSON content.

Signed-off-by: ebadiere <[email protected]>

fix: removed .only

Signed-off-by: ebadiere <[email protected]>

fix: Cleaned up file and env var evaluation.

Signed-off-by: ebadiere <[email protected]>

fix: Flaky unit test fix.

Signed-off-by: ebadiere <[email protected]>

feat: Refactored implementation and updated tests.

Signed-off-by: ebadiere <[email protected]>

* fix: Adding updated package-lock.json

Signed-off-by: ebadiere <[email protected]>

* fix: Updated global variable reference to the new HBAR_SPENDING_PLANS_CONFIG

Signed-off-by: ebadiere <[email protected]>

fix: Test fix.

Signed-off-by: ebadiere <[email protected]>

fix: Removed irrelevant test since we now use either env var or file for spending plan.

Signed-off-by: ebadiere <[email protected]>

fix: Updated HBAR_SPENDING_PLANS_CONFIG from HBAR_SPENDING_PLANS_CONFIG_FILE

Signed-off-by: ebadiere <[email protected]>

* fix: Clean up. Updated logging and appropriate tests.

Signed-off-by: ebadiere <[email protected]>

* fix: Replaced the useInMemoryRedisServer with the start and stop redis in the
before and after mocha functions.

Signed-off-by: ebadiere <[email protected]>

* fix: Test fix.  Added the envName back to the loggerService test.

Signed-off-by: ebadiere <[email protected]>

* Update docs/configuration.md

Co-authored-by: Victor Yanev <[email protected]>
Signed-off-by: Eric Badiere <[email protected]>

* fix: Added back file not found tests.

Signed-off-by: ebadiere <[email protected]>

* fix: Updated file name in `withOverriddenEnvsInMochaTest` with existing file.

Signed-off-by: ebadiere <[email protected]>

* fix: Clear the spending plan repository in a test as in CI it seems to already
be populated.

Signed-off-by: ebadiere <[email protected]>

fix: Cleanup.

Signed-off-by: ebadiere <[email protected]>

* fix: Added more time for the HBar Rate Limiter to update expenses in the background.

Signed-off-by: ebadiere <[email protected]>

* chore: divided hbar limtier tests into different batches

Signed-off-by: Logan Nguyen <[email protected]>

* fix: Removed the clearing of the spending plans.

Signed-off-by: ebadiere <[email protected]>

* chore: divided hbar limtier tests into different batches (#3181)

* chore: divided hbar limtier tests into different batches

Signed-off-by: Logan Nguyen <[email protected]>

* fix: fixed acceptance.yml

Signed-off-by: Logan Nguyen <[email protected]>

---------

Signed-off-by: Logan Nguyen <[email protected]>

* Update packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts

Co-authored-by: Nana Essilfie-Conduah <[email protected]>
Signed-off-by: Eric Badiere <[email protected]>

* fix: Restored class comments and corrected HBAR_SPENDING_PLAN_CONFIG.

Signed-off-by: ebadiere <[email protected]>

* test: fix hbarSpendingPlanConfigService.spec.ts

Signed-off-by: Victor Yanev <[email protected]>

* test: remove `.only` from `describe`

Signed-off-by: Victor Yanev <[email protected]>

* test: disconnect redis client after tests with shared cache

Signed-off-by: Victor Yanev <[email protected]>

---------

Signed-off-by: ebadiere <[email protected]>
Signed-off-by: Eric Badiere <[email protected]>
Signed-off-by: Logan Nguyen <[email protected]>
Signed-off-by: Victor Yanev <[email protected]>
Co-authored-by: Victor Yanev <[email protected]>
Co-authored-by: Logan Nguyen <[email protected]>
Co-authored-by: Nana Essilfie-Conduah <[email protected]>
Co-authored-by: Victor Yanev <[email protected]>
  • Loading branch information
5 people committed Oct 31, 2024
1 parent 1af16a2 commit d110979
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 66 deletions.
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Unless you need to set a non-default value, it is recommended to only populate o
| `HBAR_RATE_LIMIT_BASIC` | "1120000000" | Individual limit (in tinybars) for spending plans with a BASIC tier. Defaults to 11.2 HBARs. |
| `HBAR_RATE_LIMIT_EXTENDED` | "3200000000" | Individual limit (in tinybars) for spending plans with a EXTENDED tier. Defaults to 32 HBARs. |
| `HBAR_RATE_LIMIT_PRIVILEGED` | "8000000000" | Individual limit (in tinybars) for spending plans with a PRIVILEGED tier. Defaults to 80 HBARs. |
| `HBAR_SPENDING_PLANS_CONFIG_FILE` | "spendingPlansConfig.json" | The name of the JSON file containing the pre-configured spending plans for supported projects and partner projects. |
| `HBAR_SPENDING_PLANS_CONFIG` | "spendingPlansConfig.json" | The environment variable that either points to a file containing the spending plans, or the JSON content defining the spending plans. |
| `HAPI_CLIENT_DURATION_RESET` | "3600000" | Time until client reinitialization. (ms) |
| `HAPI_CLIENT_ERROR_RESET` | [21, 50] | Array of status codes, which when encountered will trigger a reinitialization. Status codes are availble [here](https://github.com/hashgraph/hedera-protobufs/blob/main/services/response_code.proto). |
| `HAPI_CLIENT_TRANSACTION_RESET` | "50" | Number of transaction executions, until client reinitialization. |
Expand Down
13 changes: 11 additions & 2 deletions docs/design/hbar-limiter.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
- [Early Detection and Prevention (Preemptive Rate Limit)](#early-detection-and-prevention-preemptive-rate-limit)
- [Architecture](#architecture)
- [High-Level Design](#high-level-design)
- [What is an HbarSpendingPlan?](#what-is-an-hbarspendingplan)
- [General Users (BASIC tier):](#general-users-basic-tier)
- [Supported Projects (EXTENDED tier) and Trusted Partners (PRIVILEGED tier):](#supported-projects-extended-tier-and-trusted-partners-privileged-tier)
- [Class Diagram](#class-diagram)
- [Service Layer](#service-layer)
- [Database Layer:](#database-layer)
Expand All @@ -21,6 +24,12 @@
- [Allocation Algorithm](#allocation-algorithm)
- [Configurations](#configurations)
- [Pre-populating the Cache with Spending Plans for Supported Projects and Partner Projects](#pre-populating-the-cache-with-spending-plans-for-supported-projects-and-partner-projects)
- [JSON Configuration File](#json-configuration-file)
- [The JSON file should have the following structure:](#the-json-file-should-have-the-following-structure)
- [Important notes](#important-notes)
- [Incremental changes to the JSON file](#incremental-changes-to-the-json-file)
- [Adding new partners or supported projects](#adding-new-partners-or-supported-projects)
- [Removing or updating existing partners or supported projects](#removing-or-updating-existing-partners-or-supported-projects)
- [Spending Limits of Different Tiers](#spending-limits-of-different-tiers)
- [Total Budget and Limit Duration](#total-budget-and-limit-duration)
- [Additional Considerations](#additional-considerations)
Expand Down Expand Up @@ -319,8 +328,8 @@ All other users (ETH and IP addresses which are not specified in the configurati

The relay will read the pre-configured spending plans from a JSON file. This file should be placed in the root directory of the relay.

The default filename for the configuration file is `spendingPlansConfig.json`, but it could also be specified by the environment variable `HBAR_SPENDING_PLANS_CONFIG_FILE`.
- `HBAR_SPENDING_PLANS_CONFIG_FILE`: The name of the file containing the pre-configured spending plans for supported projects and partners.
The default filename for the configuration file is `spendingPlansConfig.json`, but it could also be specified by the environment variable `HBAR_SPENDING_PLANS_CONFIG`.
- `HBAR_SPENDING_PLANS_CONFIG`: The name of the file or environment variable containing the pre-configured spending plans for supported projects and partners.

#### The JSON file should have the following structure:
```json
Expand Down
4 changes: 2 additions & 2 deletions packages/config-service/src/services/globalConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,8 @@ export class GlobalConfig {
required: true,
defaultValue: null,
},
HBAR_SPENDING_PLANS_CONFIG_FILE: {
envName: 'HBAR_SPENDING_PLANS_CONFIG_FILE',
HBAR_SPENDING_PLANS_CONFIG: {
envName: 'HBAR_SPENDING_PLANS_CONFIG',
type: 'string',
required: false,
defaultValue: 'spendingPlansConfig.json',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('LoggerService tests', async function () {
});

it('should be able to mask every value if it starts with known secret prefix', async () => {
const { envName } = GlobalConfig.ENTRIES.HBAR_SPENDING_PLANS_CONFIG_FILE;
const { envName } = GlobalConfig.ENTRIES.OPERATOR_KEY_MAIN;

for (const prefix of LoggerService.KNOWN_SECRET_PREFIXES) {
const value = prefix + '_VVurqVVh68wgxgcVjrvVVVcNcVVVVi3CRwl1';
Expand Down
1 change: 1 addition & 0 deletions packages/relay/src/lib/clients/cache/localLRUCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export class LocalLRUCache implements ICacheClient {
*/
public async clear(): Promise<void> {
this.cache.clear();
this.reservedCache?.clear();
}

/**
Expand Down
37 changes: 27 additions & 10 deletions packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'
* It reads the pre-configured spending plans from a JSON file and populates the cache with them.
*
* @see SpendingPlanConfig
* @see SPENDING_PLANS_CONFIG
*/
export class HbarSpendingPlanConfigService {
/**
Expand Down Expand Up @@ -127,17 +128,33 @@ export class HbarSpendingPlanConfigService {
* @private
*/
private static loadSpendingPlansConfig(logger: Logger): SpendingPlanConfig[] {
const filename = String(ConfigService.get('HBAR_SPENDING_PLANS_CONFIG_FILE'));
const configPath = findConfig(filename);
if (!configPath || !fs.existsSync(configPath)) {
logger.trace(`Configuration file not found at path "${configPath ?? filename}"`);
const spendingPlanConfig = ConfigService.get('HBAR_SPENDING_PLANS_CONFIG') as string;

if (!spendingPlanConfig) {
logger.trace('HBAR_SPENDING_PLANS_CONFIG is undefined');

Check warning on line 134 in packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts

View check run for this annotation

Codecov / codecov/patch

packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts#L134

Added line #L134 was not covered by tests
return [];
}

// Try to parse the value directly as JSON
try {
const rawData = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(rawData) as SpendingPlanConfig[];
} catch (error: any) {
throw new Error(`Failed to parse JSON from ${configPath}: ${error.message}`);
return JSON.parse(spendingPlanConfig) as SpendingPlanConfig[];
} catch (jsonParseError: any) {
// If parsing as JSON fails, treat it as a file path
logger.trace(
`Failed to parse HBAR_SPENDING_PLAN as JSON: ${jsonParseError.message}, now treating it as a file path...`,
);
try {
const configFilePath = findConfig(spendingPlanConfig);
if (configFilePath && fs.existsSync(configFilePath)) {
const fileContent = fs.readFileSync(configFilePath, 'utf-8');
return JSON.parse(fileContent) as SpendingPlanConfig[];
} else {
logger.trace(`HBAR Spending Configuration file not found at path "${configFilePath ?? spendingPlanConfig}"`);
return [];
}
} catch (fileError: any) {
throw new Error(`File error: ${fileError.message}`);
}
}
}

Expand Down Expand Up @@ -319,7 +336,7 @@ export class HbarSpendingPlanConfigService {
/**
* Deletes obsolete ETH address associations from the cache.
*
* For example, if an ETH address is associated with a plan different from the one in the {@link SPENDING_PLANS_CONFIG_FILE},
* For example, if an ETH address is associated with a plan different from the one in the {@link SPENDING_PLANS_CONFIG},
* the association is deleted from the cache to allow the new association from the configuration file to take effect.
*
* @param {SpendingPlanConfig} planConfig - The spending plan configuration.
Expand Down Expand Up @@ -347,7 +364,7 @@ export class HbarSpendingPlanConfigService {
/**
* Deletes obsolete IP address associations from the cache.
*
* For example, if an IP address is associated with a plan different from the one in the {@link SPENDING_PLANS_CONFIG_FILE},
* For example, if an IP address is associated with a plan different from the one in the {@link SPENDING_PLANS_CONFIG},
* the association is deleted from the cache to allow the new association from the configuration file to take effect.
*
* @param {SpendingPlanConfig} planConfig - The spending plan configuration.
Expand Down
107 changes: 68 additions & 39 deletions packages/relay/tests/lib/config/hbarSpendingPlanConfigService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { SpendingPlanConfig } from '../../../src/lib/types/spendingPlanConfig';
import { RequestDetails } from '../../../src/lib/types';
import { overrideEnvsInMochaDescribe, toHex, useInMemoryRedisServer, verifyResult } from '../../helpers';
import {
overrideEnvsInMochaDescribe,
toHex,
useInMemoryRedisServer,
verifyResult,
withOverriddenEnvsInMochaTest,
} from '../../helpers';
import findConfig from 'find-config';
import { HbarSpendingPlanConfigService } from '../../../src/lib/config/hbarSpendingPlanConfigService';
import { CacheService } from '../../../src/lib/services/cacheService/cacheService';
Expand Down Expand Up @@ -62,7 +68,7 @@ describe('HbarSpendingPlanConfigService', function () {
const path = findConfig(spendingPlansConfigFile);
const spendingPlansConfig = JSON.parse(fs.readFileSync(path!, 'utf-8')) as SpendingPlanConfig[];

const tests = (isSharedCacheEnabled: boolean) => {
const tests = (hbarSpendingPlansConfigEnv: string) => {
let cacheService: CacheService;
let hbarSpendingPlanRepository: HbarSpendingPlanRepository;
let ethAddressHbarSpendingPlanRepository: EthAddressHbarSpendingPlanRepository;
Expand All @@ -76,18 +82,12 @@ describe('HbarSpendingPlanConfigService', function () {
let ipAddressHbarSpendingPlanRepositorySpy: sinon.SinonSpiedInstance<IPAddressHbarSpendingPlanRepository>;

overrideEnvsInMochaDescribe({
HBAR_SPENDING_PLANS_CONFIG_FILE: spendingPlansConfigFile,
HBAR_SPENDING_PLANS_CONFIG: hbarSpendingPlansConfigEnv,
CACHE_TTL: '100',
CACHE_MAX: spendingPlansConfig.length.toString(),
});

if (isSharedCacheEnabled) {
useInMemoryRedisServer(logger, 6384);
} else {
overrideEnvsInMochaDescribe({ REDIS_ENABLED: 'false' });
}

before(function () {
before(async function () {
const reservedKeys = HbarSpendingPlanConfigService.getPreconfiguredSpendingPlanKeys(logger);
cacheService = new CacheService(logger.child({ name: 'cache-service' }), registry, reservedKeys);
hbarSpendingPlanRepository = new HbarSpendingPlanRepository(
Expand All @@ -110,7 +110,13 @@ describe('HbarSpendingPlanConfigService', function () {
);
});

beforeEach(function () {
after(async function () {
if (ConfigService.get('REDIS_ENABLED')) {
await cacheService.disconnectRedisClient();
}
});

beforeEach(async function () {
loggerSpy = sinon.spy(logger);
cacheServiceSpy = sinon.spy(cacheService);
hbarSpendingPlanRepositorySpy = sinon.spy(hbarSpendingPlanRepository);
Expand All @@ -130,12 +136,19 @@ describe('HbarSpendingPlanConfigService', function () {
await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).not.to.be.rejected;
});

it('should throw an error if configuration file is not a parsable JSON', async function () {
sinon.stub(fs, 'readFileSync').returns('invalid JSON');
await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith(
`Failed to parse JSON from ${path}: Unexpected token 'i', "invalid JSON" is not valid JSON`,
);
});
withOverriddenEnvsInMochaTest(
{
HBAR_SPENDING_PLANS_CONFIG: spendingPlansConfigFile,
},
() => {
it('should throw an error if configuration file is not a parsable JSON', async function () {
sinon.stub(fs, 'readFileSync').returns('invalid JSON');
await expect(
hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(),
).to.be.eventually.rejectedWith(`Unexpected token 'i', "invalid JSON" is not valid JSON`);
});
},
);

it('should throw an error if the configuration file has entry without ID', async function () {
const invalidPlan = {
Expand All @@ -147,9 +160,9 @@ describe('HbarSpendingPlanConfigService', function () {
.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any)
.returns([...spendingPlansConfig, invalidPlan]);

await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith(
`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`,
);
await expect(
hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(),
).to.be.eventually.rejectedWith(`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`);
});

it('should throw an error if the configuration file has entry without name', async function () {
Expand All @@ -162,9 +175,9 @@ describe('HbarSpendingPlanConfigService', function () {
.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any)
.returns([...spendingPlansConfig, invalidPlan]);

await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith(
`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`,
);
await expect(
hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(),
).to.be.eventually.rejectedWith(`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`);
});

it('should throw an error if the configuration file has entry without subscriptionTier', async function () {
Expand All @@ -177,9 +190,9 @@ describe('HbarSpendingPlanConfigService', function () {
.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any)
.returns([...spendingPlansConfig, invalidPlan]);

await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith(
`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`,
);
await expect(
hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(),
).to.be.eventually.rejectedWith(`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`);
});

it('should throw an error if the configuration file has entry with invalid subscriptionTier', async function () {
Expand All @@ -193,9 +206,9 @@ describe('HbarSpendingPlanConfigService', function () {
.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any)
.returns([...spendingPlansConfig, invalidPlan]);

await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith(
`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`,
);
await expect(
hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(),
).to.be.eventually.rejectedWith(`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`);
});

it('should throw an error if the configuration file has entry without ethAddresses and ipAddresses', async function () {
Expand All @@ -208,9 +221,9 @@ describe('HbarSpendingPlanConfigService', function () {
.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any)
.returns([...spendingPlansConfig, invalidPlan]);

await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith(
`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`,
);
await expect(
hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(),
).to.be.eventually.rejectedWith(`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`);
});

it('should throw an error if the configuration file has entry with empty ethAddresses and ipAddresses', async function () {
Expand All @@ -225,9 +238,9 @@ describe('HbarSpendingPlanConfigService', function () {
.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any)
.returns([...spendingPlansConfig, invalidPlan]);

await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith(
`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`,
);
await expect(
hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(),
).to.be.eventually.rejectedWith(`Invalid spending plan configuration: ${JSON.stringify(invalidPlan)}`);
});
});

Expand Down Expand Up @@ -651,11 +664,27 @@ describe('HbarSpendingPlanConfigService', function () {
});
};

describe('with shared cache enabled', function () {
tests(true);
describe('using Redis cache', function () {
useInMemoryRedisServer(logger, 6384);

describe('and with a spending plan config file', function () {
tests(spendingPlansConfigFile);
});

describe('and with a spending plan config variable', function () {
tests(JSON.stringify(spendingPlansConfig));
});
});

describe('with shared cache disabled', function () {
tests(false);
describe('using LRU cache', function () {
overrideEnvsInMochaDescribe({ REDIS_ENABLED: false });

describe('and with a spending plan config file', function () {
tests(spendingPlansConfigFile);
});

describe('and with a spending plan config variable', function () {
tests(JSON.stringify(spendingPlansConfig));
});
});
});
Loading

0 comments on commit d110979

Please sign in to comment.