Skip to content

Commit 9a7e410

Browse files
authored
Merge pull request #56 from pinto-org/pp/sg-proxy-performance
Improve proxy performance
2 parents 4c0b1bc + baebd09 commit 9a7e410

File tree

13 files changed

+87
-45
lines changed

13 files changed

+87
-45
lines changed

docker/docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,3 @@ services:
3232
command: ["redis-server", "--appendonly", "yes"]
3333
volumes:
3434
- ./.data/${DOCKER_ENV}/redis:/data
35-
restart: unless-stopped

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/datasources/subgraph-client.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ class SubgraphClients {
1616

1717
// let callNumber = 1;
1818
static fromUrl(url) {
19-
return async (query) => {
19+
const requestFunction = async (query) => {
2020
const client = this._getClient(url);
21-
const response = await client.request(query);
21+
const res = await client.rawRequest(query);
22+
23+
// Attach response metadata to the client function
24+
requestFunction.meta = {
25+
version: res.headers.get('x-version'),
26+
deployment: res.headers.get('x-deployment'),
27+
indexedBlock: res.headers.get('x-indexed-block')
28+
};
2229

2330
// if (EnvUtil.getDeploymentEnv().includes('local')) {
2431
// // Use this to assist in mocking. Should be commented in/out as needed.
@@ -28,8 +35,9 @@ class SubgraphClients {
2835
// );
2936
// console.log('wrote subgraph output to test directory');
3037
// }
31-
return response;
38+
return res.data;
3239
};
40+
return requestFunction;
3341
}
3442

3543
static _getClient(url) {

src/repository/postgres/startup-seeders/apy-seeder.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class ApySeeder {
2222
// Calculate and save all vapys for each season (this will take a long time for many seasons)
2323
const TAG = Concurrent.tag('apySeeder');
2424
for (const season of missingSeasons) {
25-
await Concurrent.run(TAG, 3, async () => {
25+
await Concurrent.run(TAG, 1, async () => {
2626
try {
2727
await YieldService.saveSeasonalApys({ season });
2828
} catch (e) {

src/repository/subgraph/subgraph-cache.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
const { C } = require('../../constants/runtime-constants');
22
const redisClient = require('../../datasources/redis-client');
3+
const { sendWebhookMessage } = require('../../utils/discord');
34
const Log = require('../../utils/logging');
45
const SubgraphQueryUtil = require('../../utils/subgraph-query');
56
const { SG_CACHE_CONFIG } = require('./cache-config');
67
const CommonSubgraphRepository = require('./common-subgraph');
78

89
// Caches past season results for configured queries, enabling retrieval of the full history to be fast
910
class SubgraphCache {
11+
// Introspection is required at runtime to build the schema.
12+
// If the schema of an underlying subgraph changes, the API must be redeployed (or apollo restarted).
13+
// Therefore the schema can be cached here rather than retrieved at runtime on each request.
14+
static initialIntrospection = {};
15+
static introspectionDeployment = {};
16+
1017
static async get(cacheQueryName, where) {
1118
const sgName = SG_CACHE_CONFIG[cacheQueryName].subgraph;
1219

13-
const introspection = await this.introspect(sgName);
20+
const introspection = this.initialIntrospection[sgName];
1421

1522
const { latest, cache } = await this._getCachedResults(cacheQueryName, where);
1623
const freshResults = await this._queryFreshResults(cacheQueryName, where, latest, introspection);
@@ -64,14 +71,17 @@ class SubgraphCache {
6471
}
6572

6673
if (!fromCache) {
67-
Log.info(`New deployment detected; clearing subgraph cache for ${sgName}`);
68-
await this.clear(sgName);
69-
70-
await redisClient.set(`sg-deployment:${sgName}`, deployment);
71-
await redisClient.set(`sg-introspection:${sgName}`, JSON.stringify(queryInfo));
74+
await this._newDeploymentDetected(sgName, deployment);
7275
}
7376

74-
return queryInfo;
77+
this.introspectionDeployment[sgName] = deployment;
78+
return (this.initialIntrospection[sgName] = queryInfo);
79+
}
80+
81+
static async _newDeploymentDetected(sgName, deployment) {
82+
Log.info(`New deployment detected; clearing subgraph cache for ${sgName}`);
83+
await this.clear(sgName);
84+
await redisClient.set(`sg-deployment:${sgName}`, deployment);
7585
}
7686

7787
// Recursively build a type string to use in the re-exported schema
@@ -88,7 +98,8 @@ class SubgraphCache {
8898

8999
static async _getCachedResults(cacheQueryName, where) {
90100
const cfg = SG_CACHE_CONFIG[cacheQueryName];
91-
const cachedResults = JSON.parse(await redisClient.get(`sg:${cfg.subgraph}:${cacheQueryName}:${where}`)) ?? [];
101+
const redisResult = await redisClient.get(`sg:${cfg.subgraph}:${cacheQueryName}:${where}`);
102+
const cachedResults = JSON.parse(redisResult) ?? [];
92103

93104
return {
94105
latest:
@@ -101,8 +112,9 @@ class SubgraphCache {
101112

102113
static async _queryFreshResults(cacheQueryName, where, latestValue, introspection, c = C()) {
103114
const cfg = SG_CACHE_CONFIG[cacheQueryName];
115+
const sgClient = cfg.client(c);
104116
const results = await SubgraphQueryUtil.allPaginatedSG(
105-
cfg.client(c),
117+
sgClient,
106118
`{ ${cfg.queryName} { ${introspection[cacheQueryName].fields
107119
.filter((f) => !cfg.omitFields?.includes(f.name))
108120
.concat(cfg.syntheticFields?.map((f) => ({ name: f.queryAccessor })) ?? [])
@@ -113,6 +125,14 @@ class SubgraphCache {
113125
{ ...cfg.paginationSettings, lastValue: latestValue }
114126
);
115127

128+
// If new deployment detected, clear the cache and send an alert that API might need restarting
129+
if (sgClient.meta.deployment !== this.introspectionDeployment[cfg.subgraph]) {
130+
sendWebhookMessage(
131+
`New deployment detected for ${cfg.subgraph}, the API might need to be restarted (if the schema changed).`
132+
);
133+
await this._newDeploymentDetected(cfg.subgraph, sgClient.meta.deployment);
134+
}
135+
116136
for (const result of results) {
117137
for (const syntheticField of cfg.syntheticFields ?? []) {
118138
result[syntheticField.objectRewritePath] = syntheticField.objectAccessor(result);

src/scheduled/tasks/IndexingTask.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class IndexingTask {
4040
return {
4141
countEvents,
4242
queuedCallersBehind: this._queueCounter > localCount,
43-
canExecuteAgain: !this.isCaughtUp()
43+
canExecuteAgain: !this.isCaughtUp() && countEvents !== false // false indicates task skipped
4444
};
4545
} finally {
4646
this._running = false;

test/apy/silo-apy.test.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const GaugeApyUtil = require('../../src/service/utils/apy/gauge');
88
const PreGaugeApyUtil = require('../../src/service/utils/apy/pre-gauge');
99
const { toBigInt } = require('../../src/utils/number');
1010
const { mockBeanstalkConstants } = require('../util/mock-constants');
11-
const { mockBeanstalkSG } = require('../util/mock-sg');
11+
const { mockBeanstalkSG, mockWrappedSgReturnData } = require('../util/mock-sg');
1212

1313
describe('Window EMA', () => {
1414
beforeEach(() => {
@@ -20,7 +20,7 @@ describe('Window EMA', () => {
2020

2121
it('should calculate window EMA', async () => {
2222
const rewardMintResponse = require('../mock-responses/subgraph/silo-apy/siloHourlyRewardMints_1.json');
23-
jest.spyOn(mockBeanstalkSG, 'request').mockResolvedValue(rewardMintResponse);
23+
jest.spyOn(mockBeanstalkSG, 'rawRequest').mockResolvedValue(mockWrappedSgReturnData(rewardMintResponse));
2424

2525
const emaResult = await SiloApyService.calcWindowEMA(21816, [24, 168, 720]);
2626

@@ -45,7 +45,7 @@ describe('Window EMA', () => {
4545

4646
it('should use up to as many season as are available', async () => {
4747
const rewardMintResponse = require('../mock-responses/subgraph/silo-apy/siloHourlyRewardMints_2.json');
48-
jest.spyOn(mockBeanstalkSG, 'request').mockResolvedValue(rewardMintResponse);
48+
jest.spyOn(mockBeanstalkSG, 'rawRequest').mockResolvedValue(mockWrappedSgReturnData(rewardMintResponse));
4949

5050
const emaResult = await SiloApyService.calcWindowEMA(6100, [10000, 20000]);
5151

@@ -291,9 +291,9 @@ describe('SiloApyService Orchestration', () => {
291291

292292
it('pre-gauge should supply appropriate parameters', async () => {
293293
const seasonBlockResponse = require('../mock-responses/subgraph/silo-apy/preGaugeApyInputs_1.json');
294-
jest.spyOn(mockBeanstalkSG, 'request').mockResolvedValueOnce(seasonBlockResponse);
294+
jest.spyOn(mockBeanstalkSG, 'rawRequest').mockResolvedValueOnce(mockWrappedSgReturnData(seasonBlockResponse));
295295
const preGaugeApyInputsResponse = require('../mock-responses/subgraph/silo-apy/preGaugeApyInputs_2.json');
296-
jest.spyOn(mockBeanstalkSG, 'request').mockResolvedValueOnce(preGaugeApyInputsResponse);
296+
jest.spyOn(mockBeanstalkSG, 'rawRequest').mockResolvedValueOnce(mockWrappedSgReturnData(preGaugeApyInputsResponse));
297297

298298
const spy = jest.spyOn(PreGaugeApyUtil, 'calcApy');
299299
spy.mockReturnValueOnce({
@@ -329,9 +329,9 @@ describe('SiloApyService Orchestration', () => {
329329

330330
it('gauge should supply appropriate parameters', async () => {
331331
const seasonBlockResponse = require('../mock-responses/subgraph/silo-apy/gaugeApyInputs_1.json');
332-
jest.spyOn(mockBeanstalkSG, 'request').mockResolvedValueOnce(seasonBlockResponse);
332+
jest.spyOn(mockBeanstalkSG, 'rawRequest').mockResolvedValueOnce(mockWrappedSgReturnData(seasonBlockResponse));
333333
const gaugeApyInputsResponse = require('../mock-responses/subgraph/silo-apy/gaugeApyInputs_2.json');
334-
jest.spyOn(mockBeanstalkSG, 'request').mockResolvedValueOnce(gaugeApyInputsResponse);
334+
jest.spyOn(mockBeanstalkSG, 'rawRequest').mockResolvedValueOnce(mockWrappedSgReturnData(gaugeApyInputsResponse));
335335

336336
const spy = jest.spyOn(GaugeApyUtil, 'calcApy');
337337
spy.mockReturnValueOnce({

test/repository/seeders/deposit-seeder.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const AsyncContext = require('../../../src/utils/async/context');
77
const Log = require('../../../src/utils/logging');
88
const { allToBigInt } = require('../../../src/utils/number');
99
const { mockBeanstalkConstants } = require('../../util/mock-constants');
10-
const { mockBeanstalkSG } = require('../../util/mock-sg');
10+
const { mockBeanstalkSG, mockWrappedSgReturnData } = require('../../util/mock-sg');
1111

1212
describe('Deposit Seeder', () => {
1313
beforeEach(() => {
@@ -21,7 +21,7 @@ describe('Deposit Seeder', () => {
2121
});
2222
test('Seeds all deposits', async () => {
2323
const depositsResponse = require('../../mock-responses/subgraph/silo-service/allDeposits.json');
24-
jest.spyOn(mockBeanstalkSG, 'request').mockResolvedValueOnce(depositsResponse);
24+
jest.spyOn(mockBeanstalkSG, 'rawRequest').mockResolvedValueOnce(mockWrappedSgReturnData(depositsResponse));
2525

2626
const whitelistInfoResponse = allToBigInt(require('../../mock-responses/service/whitelistedTokenInfo.json'));
2727
jest.spyOn(SiloService, 'getWhitelistedTokenInfo').mockResolvedValue(whitelistInfoResponse);

test/scheduled/sunrise.test.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const OnSunriseUtil = require('../../src/scheduled/util/on-sunrise');
22
const { mockBeanstalkConstants } = require('../util/mock-constants');
3-
const { mockBeanSG, mockBasinSG, mockBeanstalkSG } = require('../util/mock-sg');
3+
const { mockBeanSG, mockBasinSG, mockBeanstalkSG, mockWrappedSgReturnData } = require('../util/mock-sg');
44

55
async function checkLastPromiseResult(spy, expected) {
66
const lastCallResult = await spy.mock.results[spy.mock.results.length - 1].value;
@@ -22,19 +22,21 @@ describe('OnSunrise', () => {
2222

2323
it('identifies when the subgraphs have processed the new season', async () => {
2424
const seasonResponse = require('../mock-responses/subgraph/scheduled/sunrise/beanstalkSeason_1.json');
25-
const beanstalkSGSpy = jest.spyOn(mockBeanstalkSG, 'request').mockResolvedValue(seasonResponse);
25+
const beanstalkSGSpy = jest
26+
.spyOn(mockBeanstalkSG, 'rawRequest')
27+
.mockResolvedValue(mockWrappedSgReturnData(seasonResponse));
2628

2729
const metaNotReady = require('../mock-responses/subgraph/scheduled/sunrise/metaNotReady.json');
28-
const beanSGSpy = jest.spyOn(mockBeanSG, 'request').mockResolvedValue(metaNotReady);
29-
const basinSGSpy = jest.spyOn(mockBasinSG, 'request').mockResolvedValue(metaNotReady);
30+
const beanSGSpy = jest.spyOn(mockBeanSG, 'rawRequest').mockResolvedValue(mockWrappedSgReturnData(metaNotReady));
31+
const basinSGSpy = jest.spyOn(mockBasinSG, 'rawRequest').mockResolvedValue(mockWrappedSgReturnData(metaNotReady));
3032

3133
const checkSpy = jest.spyOn(OnSunriseUtil, 'checkSubgraphsForSunrise');
3234

3335
const waitPromise = OnSunriseUtil.waitForSunrise(17501, 5 * 60 * 1000);
3436
await checkLastPromiseResult(checkSpy, false);
3537

3638
const seasonResponse2 = require('../mock-responses/subgraph/scheduled/sunrise/beanstalkSeason_2.json');
37-
beanstalkSGSpy.mockResolvedValue(seasonResponse2);
39+
beanstalkSGSpy.mockResolvedValue(mockWrappedSgReturnData(seasonResponse2));
3840
// Fast-forward timers and continue
3941
jest.advanceTimersByTime(5000);
4042
jest.runAllTimers();
@@ -55,7 +57,7 @@ describe('OnSunrise', () => {
5557

5658
test('fails to identify a new season within the time limit', async () => {
5759
const seasonResponse = require('../mock-responses/subgraph/scheduled/sunrise/beanstalkSeason_1.json');
58-
jest.spyOn(mockBeanstalkSG, 'request').mockResolvedValue(seasonResponse);
60+
jest.spyOn(mockBeanstalkSG, 'rawRequest').mockResolvedValue(mockWrappedSgReturnData(seasonResponse));
5961

6062
const checkSpy = jest.spyOn(OnSunriseUtil, 'checkSubgraphsForSunrise');
6163

test/service/exchange-service.test.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const { getTickers, getWellPriceStats, getTrades } = require('../../src/service/
55
const {
66
ADDRESSES: { BEANWETH, BEANWSTETH, WETH, BEAN }
77
} = require('../../src/constants/raw/beanstalk-eth');
8-
const { mockBasinSG } = require('../util/mock-sg');
8+
const { mockBasinSG, mockWrappedSgReturnData } = require('../util/mock-sg');
99
const LiquidityUtil = require('../../src/service/utils/pool/liquidity');
1010
const ExchangeService = require('../../src/service/exchange-service');
1111
const { mockBeanstalkConstants } = require('../util/mock-constants');
@@ -23,7 +23,7 @@ describe('ExchangeService', () => {
2323

2424
it('should return all Basin tickers in the expected format', async () => {
2525
const wellsResponse = require('../mock-responses/subgraph/basin/wells.json');
26-
jest.spyOn(mockBasinSG, 'request').mockResolvedValue(wellsResponse);
26+
jest.spyOn(mockBasinSG, 'rawRequest').mockResolvedValue(mockWrappedSgReturnData(wellsResponse));
2727
// In practice these 2 values are not necessary since the subsequent getWellPriceRange is also mocked.
2828
jest.spyOn(BasinSubgraphRepository, 'getAllTrades').mockResolvedValue(undefined);
2929
jest.spyOn(ExchangeService, 'priceEventsByWell').mockReturnValueOnce(undefined);
@@ -79,7 +79,9 @@ describe('ExchangeService', () => {
7979
});
8080

8181
test('Returns swap history', async () => {
82-
jest.spyOn(mockBasinSG, 'request').mockResolvedValue(require('../mock-responses/subgraph/basin/swapHistory.json'));
82+
jest
83+
.spyOn(mockBasinSG, 'rawRequest')
84+
.mockResolvedValue(mockWrappedSgReturnData(require('../mock-responses/subgraph/basin/swapHistory.json')));
8385

8486
const options = {
8587
ticker_id: `${BEAN}_${WETH}`,

0 commit comments

Comments
 (0)