Skip to content

Commit 98dd09b

Browse files
authored
perf: Use Apify-provided environment variables to obtain PPE pricing information (#483)
- closes #481
1 parent 2219990 commit 98dd09b

File tree

4 files changed

+128
-47
lines changed

4 files changed

+128
-47
lines changed

packages/actor-scraper/web-scraper/src/internals/crawler_setup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -885,9 +885,9 @@ export class CrawlerSetup implements CrawlerSetupOptions {
885885
skipLinksP,
886886
globalStoreP,
887887
logP,
888-
// eslint-disable-next-line @typescript-eslint/await-thenable
888+
889889
requestQueueP,
890-
// eslint-disable-next-line @typescript-eslint/await-thenable
890+
891891
keyValueStoreP,
892892
]);
893893

packages/apify/src/charging.ts

Lines changed: 87 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -86,44 +86,45 @@ export class ChargingManager {
8686

8787
private apifyClient: ApifyClient;
8888

89-
constructor(configuration: Configuration, apifyClient: ApifyClient) {
89+
constructor(
90+
private configuration: Configuration,
91+
apifyClient: ApifyClient,
92+
) {
9093
this.maxTotalChargeUsd =
9194
configuration.get('maxTotalChargeUsd') || Infinity; // convert `0` to `Infinity` in case the value is an empty string
9295
this.isAtHome = configuration.get('isAtHome');
9396
this.actorRunId = configuration.get('actorRunId');
9497
this.purgeChargingLogDataset = configuration.get('purgeOnStart');
9598
this.useChargingLogDataset = configuration.get('useChargingLogDataset');
9699

97-
if (this.useChargingLogDataset && this.isAtHome) {
98-
throw new Error(
99-
'Using the ACTOR_USE_CHARGING_LOG_DATASET environment variable is only supported in a local development environment',
100-
);
101-
}
102-
103-
if (configuration.get('testPayPerEvent')) {
104-
if (this.isAtHome) {
105-
throw new Error(
106-
'Using the ACTOR_TEST_PAY_PER_EVENT environment variable is only supported in a local development environment',
107-
);
108-
}
109-
110-
this.pricingModel = 'PAY_PER_EVENT';
111-
}
112-
113100
this.apifyClient = apifyClient;
114101
}
115102

116103
private get isPayPerEvent() {
117104
return this.pricingModel === 'PAY_PER_EVENT';
118105
}
119106

120-
/**
121-
* Initialize the ChargingManager by loading pricing information and charging state via Apify API.
122-
*/
123-
async init(): Promise<void> {
124-
this.chargingState = {};
107+
private async fetchPricingInfo(): Promise<{
108+
pricingInfo?: ActorRunPricingInfo;
109+
chargedEventCounts?: Record<string, number>;
110+
maxTotalChargeUsd: number;
111+
}> {
112+
if (
113+
this.configuration.get('actorPricingInfo') &&
114+
this.configuration.get('chargedEventCounts')
115+
) {
116+
return {
117+
pricingInfo: JSON.parse(
118+
this.configuration.get('actorPricingInfo'),
119+
) as ActorRunPricingInfo,
120+
chargedEventCounts: JSON.parse(
121+
this.configuration.get('chargedEventCounts'),
122+
) as Record<string, number>,
123+
maxTotalChargeUsd:
124+
this.configuration.get('maxTotalChargeUsd') || Infinity,
125+
};
126+
}
125127

126-
// Retrieve pricing information
127128
if (this.isAtHome) {
128129
if (this.actorRunId === undefined) {
129130
throw new Error(
@@ -136,33 +137,74 @@ export class ChargingManager {
136137
throw new Error('Actor run not found');
137138
}
138139

139-
this.pricingModel = run.pricingInfo?.pricingModel;
140-
141-
// Load per-event pricing information
142-
if (run.pricingInfo?.pricingModel === 'PAY_PER_EVENT') {
143-
for (const [eventName, eventPricing] of Object.entries(
144-
run.pricingInfo.pricingPerEvent.actorChargeEvents,
145-
)) {
146-
this.pricingInfo[eventName] = {
147-
price: eventPricing.eventPriceUsd,
148-
title: eventPricing.eventTitle,
149-
};
150-
}
151-
152-
this.maxTotalChargeUsd =
153-
run.options.maxTotalChargeUsd ?? this.maxTotalChargeUsd;
140+
return {
141+
pricingInfo: run.pricingInfo,
142+
chargedEventCounts: run.chargedEventCounts,
143+
maxTotalChargeUsd: run.options.maxTotalChargeUsd || Infinity,
144+
};
145+
}
146+
147+
return {
148+
pricingInfo: undefined,
149+
chargedEventCounts: {},
150+
maxTotalChargeUsd:
151+
this.configuration.get('maxTotalChargeUsd') || Infinity,
152+
};
153+
}
154+
155+
/**
156+
* Initialize the ChargingManager by loading pricing information and charging state via Apify API.
157+
*/
158+
async init(): Promise<void> {
159+
// Validate config - it may have changed since the instantiation
160+
if (this.useChargingLogDataset && this.isAtHome) {
161+
throw new Error(
162+
'Using the ACTOR_USE_CHARGING_LOG_DATASET environment variable is only supported in a local development environment',
163+
);
164+
}
165+
166+
if (this.configuration.get('testPayPerEvent')) {
167+
if (this.isAtHome) {
168+
throw new Error(
169+
'Using the ACTOR_TEST_PAY_PER_EVENT environment variable is only supported in a local development environment',
170+
);
154171
}
172+
}
173+
174+
// Retrieve pricing information
175+
const { pricingInfo, chargedEventCounts, maxTotalChargeUsd } =
176+
await this.fetchPricingInfo();
177+
178+
if (this.configuration.get('testPayPerEvent')) {
179+
this.pricingModel = 'PAY_PER_EVENT';
180+
} else {
181+
this.pricingModel ??= pricingInfo?.pricingModel;
182+
}
155183

156-
// Load charged event counts
157-
for (const [eventName, chargeCount] of Object.entries(
158-
run.chargedEventCounts ?? {},
184+
// Load per-event pricing information
185+
if (pricingInfo?.pricingModel === 'PAY_PER_EVENT') {
186+
for (const [eventName, eventPricing] of Object.entries(
187+
pricingInfo.pricingPerEvent.actorChargeEvents,
159188
)) {
160-
this.chargingState[eventName] = {
161-
chargeCount,
162-
totalChargedAmount:
163-
chargeCount * (this.pricingInfo[eventName]?.price ?? 0),
189+
this.pricingInfo[eventName] = {
190+
price: eventPricing.eventPriceUsd,
191+
title: eventPricing.eventTitle,
164192
};
165193
}
194+
195+
this.maxTotalChargeUsd = maxTotalChargeUsd;
196+
}
197+
198+
this.chargingState = {};
199+
200+
for (const [eventName, chargeCount] of Object.entries(
201+
chargedEventCounts ?? {},
202+
)) {
203+
this.chargingState[eventName] = {
204+
chargeCount,
205+
totalChargedAmount:
206+
chargeCount * (this.pricingInfo[eventName]?.price ?? 0),
207+
};
166208
}
167209

168210
if (!this.isPayPerEvent || !this.useChargingLogDataset) {

packages/apify/src/configuration.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export interface ConfigurationOptions extends CoreConfigurationOptions {
3939
metaOrigin?: (typeof META_ORIGINS)[keyof typeof META_ORIGINS];
4040
testPayPerEvent?: boolean;
4141
useChargingLogDataset?: boolean;
42+
actorPricingInfo?: string;
43+
chargedEventCounts?: string;
4244
}
4345

4446
/**
@@ -179,6 +181,8 @@ export class Configuration extends CoreConfiguration {
179181
ACTOR_MAX_TOTAL_CHARGE_USD: 'maxTotalChargeUsd',
180182
ACTOR_TEST_PAY_PER_EVENT: 'testPayPerEvent',
181183
ACTOR_USE_CHARGING_LOG_DATASET: 'useChargingLogDataset',
184+
APIFY_ACTOR_PRICING_INFO: 'actorPricingInfo',
185+
APIFY_CHARGED_ACTOR_EVENT_COUNTS: 'chargedEventCounts',
182186
};
183187

184188
protected static override INTEGER_VARS = [

test/apify/charging.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ describe('ChargingManager', () => {
1414

1515
afterEach(async () => {
1616
await Actor.exit({ exit: false });
17+
18+
// @ts-expect-error
19+
Actor._instance = undefined; // eslint-disable-line no-underscore-dangle
20+
1721
await localStorageEmulator.destroy();
1822

1923
delete process.env.ACTOR_TEST_PAY_PER_EVENT;
2024
delete process.env.ACTOR_MAX_TOTAL_CHARGE_USD;
25+
delete process.env.APIFY_ACTOR_PRICING_INFO;
26+
delete process.env.APIFY_CHARGED_ACTOR_EVENT_COUNTS;
2127
});
2228

2329
describe('charge()', () => {
@@ -40,5 +46,34 @@ describe('ChargingManager', () => {
4046
expect(zeroResult.eventChargeLimitReached).toBe(false);
4147
expect(zeroResult.chargedCount).toBe(0);
4248
});
49+
50+
test('should charge events when ACTOR_MAX_TOTAL_CHARGE_USD is set to "" (cost-unlimited)', async () => {
51+
process.env.ACTOR_MAX_TOTAL_CHARGE_USD = '';
52+
53+
// Set pricing info via env vars (as if coming from platform)
54+
process.env.APIFY_ACTOR_PRICING_INFO = JSON.stringify({
55+
pricingModel: 'PAY_PER_EVENT',
56+
pricingPerEvent: {
57+
actorChargeEvents: {
58+
foobar: {
59+
eventTitle: 'Foo bar',
60+
eventPriceUsd: 0.1,
61+
eventDescription: 'Foo foo bar bar',
62+
},
63+
},
64+
},
65+
});
66+
process.env.APIFY_CHARGED_ACTOR_EVENT_COUNTS = JSON.stringify({});
67+
68+
await Actor.init();
69+
70+
const chargeResult = await Actor.charge({
71+
eventName: 'foobar',
72+
count: 4,
73+
});
74+
75+
expect(chargeResult.chargedCount).toBe(4);
76+
expect(chargeResult.eventChargeLimitReached).toBe(false);
77+
});
4378
});
4479
});

0 commit comments

Comments
 (0)