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: Added ability to load spending plans from environment variable. #3153

Merged
merged 25 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b79887a
feat: Added ability to load spening plans from environment variable.
ebadiere Oct 23, 2024
4415cb3
feat: Now uses one property for either spending plan file or JSON con…
ebadiere Oct 24, 2024
7b806d7
fix: Adding updated package-lock.json
ebadiere Oct 25, 2024
2bcd136
Merge branch 'main' into 3152-spendingplan-as-env-var
ebadiere Oct 25, 2024
f1682cb
fix: Updated global variable reference to the new HBAR_SPENDING_PLANS…
ebadiere Oct 25, 2024
d915ee0
fix: Clean up. Updated logging and appropriate tests.
ebadiere Oct 28, 2024
8c0ad26
Merge branch 'main' into 3152-spendingplan-as-env-var
ebadiere Oct 28, 2024
6beadfd
fix: Replaced the useInMemoryRedisServer with the start and stop redi…
ebadiere Oct 28, 2024
0f0eb73
fix: Test fix. Added the envName back to the loggerService test.
ebadiere Oct 28, 2024
a1570c3
Update docs/configuration.md
ebadiere Oct 29, 2024
07e99d5
fix: Added back file not found tests.
ebadiere Oct 29, 2024
174b5a5
fix: Updated file name in `withOverriddenEnvsInMochaTest` with existi…
ebadiere Oct 29, 2024
eb3f939
fix: Clear the spending plan repository in a test as in CI it seems t…
ebadiere Oct 29, 2024
3f30f72
fix: Added more time for the HBar Rate Limiter to update expenses in …
ebadiere Oct 29, 2024
863f1ab
chore: divided hbar limtier tests into different batches
quiet-node Oct 30, 2024
011aca2
Merge branch '3180-enable-hbar-limiter-acceptance-test-to-run-in-diff…
ebadiere Oct 30, 2024
5aebd00
fix: Removed the clearing of the spending plans.
ebadiere Oct 31, 2024
4b567ca
chore: divided hbar limtier tests into different batches (#3181)
quiet-node Oct 30, 2024
04b193e
Merge branch 'main' into 3152-spendingplan-as-env-var
ebadiere Oct 31, 2024
e8abee6
Update packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts
ebadiere Oct 31, 2024
0c80825
Merge branch '3152-spendingplan-as-env-var' of https://github.com/has…
ebadiere Oct 31, 2024
d1abc7b
fix: Restored class comments and corrected HBAR_SPENDING_PLAN_CONFIG.
ebadiere Oct 31, 2024
4eea649
test: fix hbarSpendingPlanConfigService.spec.ts
victor-yanev Oct 31, 2024
5e1182a
test: remove `.only` from `describe`
victor-yanev Oct 31, 2024
c48d26f
test: disconnect redis client after tests with shared cache
victor-yanev Oct 31, 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
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
39 changes: 23 additions & 16 deletions packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@
import { IDetailedHbarSpendingPlan } from '../db/types/hbarLimiter/hbarSpendingPlan';
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';

/**
ebadiere marked this conversation as resolved.
Show resolved Hide resolved
* Service for managing pre-configured {@link HbarSpendingPlan} entities.
*
* It reads the pre-configured spending plans from a JSON file and populates the cache with them.
*
* @see SpendingPlanConfig
*/
export class HbarSpendingPlanConfigService {
/**
* The time-to-live (TTL) for the pre-configured spending plans in the cache.
Expand Down Expand Up @@ -127,17 +120,31 @@
* @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}"`);
return [];
const spendingPlanConfig = ConfigService.get('HBAR_SPENDING_PLANS_CONFIG') as string;

if (!spendingPlanConfig) {
throw new Error('HBAR_SPENDING_PLANS_CONFIG is undefined');

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L126 was not covered by tests
}

// 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 {
throw new Error(`Configuration file not found at path "${configFilePath ?? spendingPlanConfig}"`);
ebadiere marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (fileError: any) {
throw new Error(`File error: ${fileError.message}`);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ 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,
startRedisInMemoryServer,
stopRedisInMemoryServer,
toHex,
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 All @@ -40,6 +47,8 @@ import {
HbarSpendingPlanNotFoundError,
IPAddressHbarSpendingPlanNotFoundError,
} from '../../../src/lib/db/types/hbarLimiter/errors';
import { ConfigServiceTestHelper } from '@hashgraph/json-rpc-config-service/tests/configServiceTestHelper';
import { RedisInMemoryServer } from '../../redisInMemoryServer';

chai.use(chaiAsPromised);

Expand All @@ -62,7 +71,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 = (isSharedCacheEnabled: boolean, isSpendingPlansConfigFile: boolean) => {
let cacheService: CacheService;
let hbarSpendingPlanRepository: HbarSpendingPlanRepository;
let ethAddressHbarSpendingPlanRepository: EthAddressHbarSpendingPlanRepository;
Expand All @@ -74,20 +83,26 @@ describe('HbarSpendingPlanConfigService', function () {
let hbarSpendingPlanRepositorySpy: sinon.SinonSpiedInstance<HbarSpendingPlanRepository>;
let ethAddressHbarSpendingPlanRepositorySpy: sinon.SinonSpiedInstance<EthAddressHbarSpendingPlanRepository>;
let ipAddressHbarSpendingPlanRepositorySpy: sinon.SinonSpiedInstance<IPAddressHbarSpendingPlanRepository>;
let redisInMemoryServer: RedisInMemoryServer;

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

if (isSharedCacheEnabled) {
useInMemoryRedisServer(logger, 6384);
if (isSpendingPlansConfigFile) {
ConfigServiceTestHelper.dynamicOverride('HBAR_SPENDING_PLANS_CONFIG', spendingPlansConfigFile);
} else {
overrideEnvsInMochaDescribe({ REDIS_ENABLED: 'false' });
ConfigServiceTestHelper.dynamicOverride('HBAR_SPENDING_PLANS_CONFIG', JSON.stringify(spendingPlansConfig));
}

before(function () {
before(async function () {
if (isSharedCacheEnabled) {
redisInMemoryServer = await startRedisInMemoryServer(logger, 6384);
} else {
overrideEnvsInMochaDescribe({ REDIS_ENABLED: 'false' });
}

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

after(async function () {
if (isSharedCacheEnabled) {
await stopRedisInMemoryServer(redisInMemoryServer);
}
});

beforeEach(function () {
loggerSpy = sinon.spy(logger);
cacheServiceSpy = sinon.spy(cacheService);
Expand All @@ -130,12 +151,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.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 Down Expand Up @@ -356,6 +384,11 @@ describe('HbarSpendingPlanConfigService', function () {
};

it('should populate the database with pre-configured spending plans', async function () {
// Clear existing spending plans. Needed in CI environment where the plans are already populated
for (const plan of spendingPlansConfig) {
await hbarSpendingPlanRepository.delete(plan.id, emptyRequestDetails);
}
ebadiere marked this conversation as resolved.
Show resolved Hide resolved

await hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans();

spendingPlansConfig.forEach(({ id, name, subscriptionTier }) => {
Expand Down Expand Up @@ -651,11 +684,23 @@ describe('HbarSpendingPlanConfigService', function () {
});
};

describe('with shared cache enabled', function () {
tests(true);
describe('using Redis cache', function () {
describe('and with a spending plan config file', function () {
tests(true, true);
});

describe('and with a spending plan config variable', function () {
tests(true, false);
});
});

describe('with shared cache disabled', function () {
tests(false);
describe('using LRU cache', function () {
describe('and with a spending plan config file', function () {
tests(false, true);
});

describe('and with a spending plan config variable', function () {
tests(false, false);
});
});
});
7 changes: 3 additions & 4 deletions packages/relay/tests/lib/relay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('RelayImpl', () => {
});

describe('when a configuration file is provided', () => {
overrideEnvsInMochaDescribe({ HBAR_SPENDING_PLANS_CONFIG_FILE: 'spendingPlansConfig.example.json' });
overrideEnvsInMochaDescribe({ HBAR_SPENDING_PLANS_CONFIG: 'spendingPlansConfig.example.json' });

it('should populate preconfigured spending plans successfully', async () => {
expect((relay = new RelayImpl(logger, register))).to.not.throw;
Expand All @@ -121,7 +121,7 @@ describe('RelayImpl', () => {
describe('when a configuration file with invalid JSON is provided', () => {
let path: string | null;

overrideEnvsInMochaDescribe({ HBAR_SPENDING_PLANS_CONFIG_FILE: 'spendingPlansConfig.example.json' });
overrideEnvsInMochaDescribe({ HBAR_SPENDING_PLANS_CONFIG: 'spendingPlansConfig.example.json' });

beforeEach(() => {
path = findConfig('spendingPlansConfig.example.json');
Expand All @@ -134,8 +134,7 @@ describe('RelayImpl', () => {
expect(populatePreconfiguredSpendingPlansSpy.calledOnce).to.be.true;
await expect(populatePreconfiguredSpendingPlansSpy.returnValues[0]).not.to.be.rejected;

const cause = `Failed to parse JSON from ${path}: Unexpected token 'i', "invalid JSON" is not valid JSON`;
const message = `Failed to load pre-configured spending plans: ${cause}`;
const message = `Failed to load pre-configured spending plans: File error: Unexpected token 'i', "invalid JSON" is not valid JSON`;
expect(loggerSpy.warn.calledWith(message)).to.be.true;
});
});
Expand Down
Loading
Loading