Skip to content

Commit 0101cb1

Browse files
authored
fix: calculate run memory min/max memory clamping (#986)
This PR updates the dynamic run memory calculation (added in #980) to respect the memory limits set in `actor.json`. The final memory value is now clamped between `minMemoryMbytes` and `maxMemoryMbytes`. In case user provides `defaultMemoryMbytes` via flag, we're not checking for min/max memory from config, because it feels confusing.
1 parent 92696be commit 0101cb1

2 files changed

Lines changed: 74 additions & 13 deletions

File tree

src/commands/actor/calculate-memory.ts

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import { getJsonFileContent, getLocalKeyValueStorePath } from '../../lib/utils.j
1212

1313
const DEFAULT_INPUT_PATH = join(getLocalKeyValueStorePath('default'), 'INPUT.json');
1414

15+
interface ActorMemoryConfig {
16+
defaultMemoryMbytes?: string;
17+
minMemoryMbytes?: number;
18+
maxMemoryMbytes?: number;
19+
}
20+
1521
/**
1622
* This command can be used to test dynamic memory calculation expressions
1723
* defined in actor.json or provided via command-line flag.
@@ -60,22 +66,15 @@ export class ActorCalculateMemoryCommand extends ApifyCommand<typeof ActorCalcul
6066
};
6167

6268
async run() {
63-
const { input, defaultMemoryMbytes, ...runOptions } = this.flags;
64-
65-
let memoryExpression: string | undefined = defaultMemoryMbytes;
66-
67-
// If not provided via flag, try to load from actor.json
68-
if (!memoryExpression) {
69-
memoryExpression = await this.getExpressionFromConfig();
70-
}
69+
const { input, memoryExpression, minMemory, maxMemory, runOptions } = await this.prepareMemoryArguments();
7170

7271
if (!memoryExpression) {
7372
throw new Error(
7473
`No memory-calculation expression found. Provide it via the --defaultMemoryMbytes flag or define defaultMemoryMbytes in actor.json.`,
7574
);
7675
}
7776

78-
const inputPath = resolve(process.cwd(), this.flags.input);
77+
const inputPath = resolve(process.cwd(), input);
7978
const inputJson = getJsonFileContent(inputPath) ?? {};
8079

8180
info({ message: `Evaluating memory expression: ${memoryExpression}` });
@@ -85,16 +84,48 @@ export class ActorCalculateMemoryCommand extends ApifyCommand<typeof ActorCalcul
8584
input: inputJson,
8685
runOptions,
8786
});
88-
success({ message: `Calculated memory: ${result} MB`, stdout: true });
87+
const clampedResult = Math.min(Math.max(result, minMemory), maxMemory);
88+
89+
success({ message: `Calculated memory: ${clampedResult} MB`, stdout: true });
8990
} catch (err) {
9091
error({ message: `Memory calculation failed: ${(err as Error).message}` });
9192
}
9293
}
9394

95+
/**
96+
* Determines the memory arguments to use.
97+
* If --defaultMemoryMbytes flag is set, use it (unlimited min/max).
98+
* Otherwise, load from actor.json.
99+
*/
100+
private async prepareMemoryArguments() {
101+
const { input, defaultMemoryMbytes, ...runOptions } = this.flags;
102+
103+
let memoryExpression: string | undefined = defaultMemoryMbytes;
104+
let minMemory = 0;
105+
let maxMemory = Infinity;
106+
107+
// If not provided via flag, try to load from actor.json
108+
if (!memoryExpression) {
109+
({
110+
defaultMemoryMbytes: memoryExpression,
111+
minMemoryMbytes: minMemory = minMemory, // Fallback to minMemory(0) if undefined
112+
maxMemoryMbytes: maxMemory = maxMemory, // Fallback to maxMemory(Infinity) if undefined
113+
} = await this.getExpressionFromConfig());
114+
}
115+
116+
return {
117+
memoryExpression,
118+
minMemory,
119+
maxMemory,
120+
input,
121+
runOptions,
122+
};
123+
}
124+
94125
/**
95126
* Helper to load the `defaultMemoryMbytes` expression from actor.json.
96127
*/
97-
private async getExpressionFromConfig(): Promise<string | undefined> {
128+
private async getExpressionFromConfig(): Promise<ActorMemoryConfig> {
98129
const cwd = process.cwd();
99130
const localConfigResult = await useActorConfig({ cwd });
100131

@@ -103,10 +134,14 @@ export class ActorCalculateMemoryCommand extends ApifyCommand<typeof ActorCalcul
103134

104135
error({ message: `${message}${cause ? `\n ${cause.message}` : ''}` });
105136
process.exitCode = CommandExitCodes.InvalidActorJson;
106-
return;
137+
return {};
107138
}
108139

109140
const { config: localConfig } = localConfigResult.unwrap();
110-
return localConfig?.defaultMemoryMbytes?.toString();
141+
return {
142+
defaultMemoryMbytes: localConfig?.defaultMemoryMbytes?.toString(),
143+
minMemoryMbytes: localConfig?.minMemoryMbytes as number | undefined,
144+
maxMemoryMbytes: localConfig?.maxMemoryMbytes as number | undefined,
145+
};
111146
}
112147
}

test/local/commands/actor/calculate-memory.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,30 @@ describe('apify actor calculate-memory', () => {
9090

9191
expect(lastErrorMessage()).toMatch(/Memory calculation failed: /);
9292
});
93+
94+
describe('clamping to minMemoryMbytes/maxMemoryMbytes', () => {
95+
it('should clamp memory to minMemoryMbytes from actor.json', async () => {
96+
await createActorJson({
97+
defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION,
98+
minMemoryMbytes: 8192,
99+
});
100+
101+
await testRunCommand(ActorCalculateMemoryCommand, {
102+
flags_input: inputPath,
103+
});
104+
expect(lastLogMessage()).toMatch(/8192 MB/);
105+
});
106+
107+
it('should clamp memory to maxMemoryMbytes from actor.json', async () => {
108+
await createActorJson({
109+
defaultMemoryMbytes: START_URLS_LENGTH_BASED_MEMORY_EXPRESSION,
110+
maxMemoryMbytes: 2048,
111+
});
112+
113+
await testRunCommand(ActorCalculateMemoryCommand, {
114+
flags_input: inputPath,
115+
});
116+
expect(lastLogMessage()).toMatch(/2048 MB/);
117+
});
118+
});
93119
});

0 commit comments

Comments
 (0)