Browse Source

change environment variables for v8 memory management

Yuki Takei 1 month ago
parent
commit
04a4eb19d7

+ 4 - 4
.kiro/specs/official-docker-image/tasks.md

@@ -171,8 +171,8 @@
 
 
 > Rename the `GROWI_`-prefixed memory management environment variables to `V8_`-prefixed names aligned with V8 option names, and add documentation to the Docker Hub README.
 > Rename the `GROWI_`-prefixed memory management environment variables to `V8_`-prefixed names aligned with V8 option names, and add documentation to the Docker Hub README.
 
 
-- [ ] 9. Rename environment variables to align with V8 option names
-- [ ] 9.1 (P) Rename all GROWI_-prefixed environment variables to V8_-prefixed names in the entrypoint
+- [x] 9. Rename environment variables to align with V8 option names
+- [x] 9.1 (P) Rename all GROWI_-prefixed environment variables to V8_-prefixed names in the entrypoint
   - Rename `GROWI_HEAP_SIZE` to `V8_MAX_HEAP_SIZE` in the heap size detection function, validation logic, and error messages
   - Rename `GROWI_HEAP_SIZE` to `V8_MAX_HEAP_SIZE` in the heap size detection function, validation logic, and error messages
   - Rename `GROWI_OPTIMIZE_MEMORY` to `V8_OPTIMIZE_FOR_SIZE` in the node flag assembly function
   - Rename `GROWI_OPTIMIZE_MEMORY` to `V8_OPTIMIZE_FOR_SIZE` in the node flag assembly function
   - Rename `GROWI_LITE_MODE` to `V8_LITE_MODE` in the node flag assembly function
   - Rename `GROWI_LITE_MODE` to `V8_LITE_MODE` in the node flag assembly function
@@ -180,13 +180,13 @@
   - Update the file header comment documenting the heap size detection fallback chain
   - Update the file header comment documenting the heap size detection fallback chain
   - _Requirements: 2.1, 2.5, 2.6, 2.7, 6.1, 6.6, 6.7_
   - _Requirements: 2.1, 2.5, 2.6, 2.7, 6.1, 6.6, 6.7_
 
 
-- [ ] 9.2 (P) Update all environment variable references in entrypoint unit tests
+- [x] 9.2 (P) Update all environment variable references in entrypoint unit tests
   - Update heap size detection tests: replace all `GROWI_HEAP_SIZE` references with `V8_MAX_HEAP_SIZE`
   - Update heap size detection tests: replace all `GROWI_HEAP_SIZE` references with `V8_MAX_HEAP_SIZE`
   - Update node flag assembly tests: replace `GROWI_OPTIMIZE_MEMORY` with `V8_OPTIMIZE_FOR_SIZE` and `GROWI_LITE_MODE` with `V8_LITE_MODE`
   - Update node flag assembly tests: replace `GROWI_OPTIMIZE_MEMORY` with `V8_OPTIMIZE_FOR_SIZE` and `GROWI_LITE_MODE` with `V8_LITE_MODE`
   - Verify all tests pass with the new environment variable names
   - Verify all tests pass with the new environment variable names
   - _Requirements: 2.1, 2.5, 2.6_
   - _Requirements: 2.1, 2.5, 2.6_
 
 
-- [ ] 10. Add V8 memory management environment variable documentation to README
+- [x] 10. Add V8 memory management environment variable documentation to README
   - Add a subsection under Configuration > Environment Variables documenting the three V8 memory management variables
   - Add a subsection under Configuration > Environment Variables documenting the three V8 memory management variables
   - Include variable name, type, default value, and description for each: `V8_MAX_HEAP_SIZE`, `V8_OPTIMIZE_FOR_SIZE`, `V8_LITE_MODE`
   - Include variable name, type, default value, and description for each: `V8_MAX_HEAP_SIZE`, `V8_OPTIMIZE_FOR_SIZE`, `V8_LITE_MODE`
   - Describe the 3-tier heap size fallback behavior (env var → cgroup auto-calculation → V8 default)
   - Describe the 3-tier heap size fallback behavior (env var → cgroup auto-calculation → V8 default)

+ 10 - 0
apps/app/docker/README.md

@@ -72,6 +72,16 @@ See [GROWI Docs: Admin Guide](https://docs.growi.org/en/admin-guide/) ([en](http
 
 
 - [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/admin-cookbook/env-vars.html)
 - [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/admin-cookbook/env-vars.html)
 
 
+#### V8 Memory Management
+
+| Variable | Type | Default | Description |
+|----------|------|---------|-------------|
+| `V8_MAX_HEAP_SIZE` | int (MB) | (unset) | Explicitly specify the `--max-heap-size` value for Node.js |
+| `V8_OPTIMIZE_FOR_SIZE` | `"true"` / (unset) | (unset) | Enable the `--optimize-for-size` V8 flag to reduce memory usage |
+| `V8_LITE_MODE` | `"true"` / (unset) | (unset) | Enable the `--lite-mode` V8 flag to reduce memory usage at the cost of performance |
+
+**Heap size fallback behavior**: When `V8_MAX_HEAP_SIZE` is not set, the entrypoint automatically detects the container's memory limit via cgroup (v2/v1) and sets the heap size to 60% of the limit. If no cgroup limit is detected, V8's default heap behavior is used.
+
 
 
 Issues
 Issues
 ------
 ------

+ 22 - 22
apps/app/docker/docker-entrypoint.spec.ts

@@ -108,8 +108,8 @@ describe('detectHeapSize', () => {
     process.env = originalEnv;
     process.env = originalEnv;
   });
   });
 
 
-  it('should use GROWI_HEAP_SIZE when set', () => {
-    process.env.GROWI_HEAP_SIZE = '512';
+  it('should use V8_MAX_HEAP_SIZE when set', () => {
+    process.env.V8_MAX_HEAP_SIZE = '512';
     const readSpy = vi.spyOn(fs, 'readFileSync');
     const readSpy = vi.spyOn(fs, 'readFileSync');
     const result = detectHeapSize();
     const result = detectHeapSize();
     expect(result).toBe(512);
     expect(result).toBe(512);
@@ -118,8 +118,8 @@ describe('detectHeapSize', () => {
     readSpy.mockRestore();
     readSpy.mockRestore();
   });
   });
 
 
-  it('should return undefined for invalid GROWI_HEAP_SIZE', () => {
-    process.env.GROWI_HEAP_SIZE = 'abc';
+  it('should return undefined for invalid V8_MAX_HEAP_SIZE', () => {
+    process.env.V8_MAX_HEAP_SIZE = 'abc';
     const readSpy = vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
     const readSpy = vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
       throw new Error('ENOENT');
       throw new Error('ENOENT');
     });
     });
@@ -128,8 +128,8 @@ describe('detectHeapSize', () => {
     readSpy.mockRestore();
     readSpy.mockRestore();
   });
   });
 
 
-  it('should return undefined for empty GROWI_HEAP_SIZE', () => {
-    process.env.GROWI_HEAP_SIZE = '';
+  it('should return undefined for empty V8_MAX_HEAP_SIZE', () => {
+    process.env.V8_MAX_HEAP_SIZE = '';
     const readSpy = vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
     const readSpy = vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
       throw new Error('ENOENT');
       throw new Error('ENOENT');
     });
     });
@@ -139,7 +139,7 @@ describe('detectHeapSize', () => {
   });
   });
 
 
   it('should auto-calculate from cgroup v2 at 60%', () => {
   it('should auto-calculate from cgroup v2 at 60%', () => {
-    delete process.env.GROWI_HEAP_SIZE;
+    delete process.env.V8_MAX_HEAP_SIZE;
     // 1GB = 1073741824 bytes → 60% ≈ 614 MB
     // 1GB = 1073741824 bytes → 60% ≈ 614 MB
     const readSpy = vi
     const readSpy = vi
       .spyOn(fs, 'readFileSync')
       .spyOn(fs, 'readFileSync')
@@ -153,7 +153,7 @@ describe('detectHeapSize', () => {
   });
   });
 
 
   it('should fallback to cgroup v1 when v2 is unlimited', () => {
   it('should fallback to cgroup v1 when v2 is unlimited', () => {
-    delete process.env.GROWI_HEAP_SIZE;
+    delete process.env.V8_MAX_HEAP_SIZE;
     // v2 = max (unlimited), v1 = 2GB
     // v2 = max (unlimited), v1 = 2GB
     const readSpy = vi
     const readSpy = vi
       .spyOn(fs, 'readFileSync')
       .spyOn(fs, 'readFileSync')
@@ -169,7 +169,7 @@ describe('detectHeapSize', () => {
   });
   });
 
 
   it('should treat cgroup v1 > 64GB as unlimited', () => {
   it('should treat cgroup v1 > 64GB as unlimited', () => {
-    delete process.env.GROWI_HEAP_SIZE;
+    delete process.env.V8_MAX_HEAP_SIZE;
     const hugeValue = 128 * 1024 * 1024 * 1024; // 128GB
     const hugeValue = 128 * 1024 * 1024 * 1024; // 128GB
     const readSpy = vi
     const readSpy = vi
       .spyOn(fs, 'readFileSync')
       .spyOn(fs, 'readFileSync')
@@ -185,7 +185,7 @@ describe('detectHeapSize', () => {
   });
   });
 
 
   it('should return undefined when no cgroup limits detected', () => {
   it('should return undefined when no cgroup limits detected', () => {
-    delete process.env.GROWI_HEAP_SIZE;
+    delete process.env.V8_MAX_HEAP_SIZE;
     const readSpy = vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
     const readSpy = vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
       throw new Error('ENOENT');
       throw new Error('ENOENT');
     });
     });
@@ -194,8 +194,8 @@ describe('detectHeapSize', () => {
     readSpy.mockRestore();
     readSpy.mockRestore();
   });
   });
 
 
-  it('should prioritize GROWI_HEAP_SIZE over cgroup', () => {
-    process.env.GROWI_HEAP_SIZE = '256';
+  it('should prioritize V8_MAX_HEAP_SIZE over cgroup', () => {
+    process.env.V8_MAX_HEAP_SIZE = '256';
     const readSpy = vi
     const readSpy = vi
       .spyOn(fs, 'readFileSync')
       .spyOn(fs, 'readFileSync')
       .mockReturnValue('1073741824\n');
       .mockReturnValue('1073741824\n');
@@ -233,33 +233,33 @@ describe('buildNodeFlags', () => {
     expect(flags.some((f) => f.startsWith('--max-heap-size'))).toBe(false);
     expect(flags.some((f) => f.startsWith('--max-heap-size'))).toBe(false);
   });
   });
 
 
-  it('should include --optimize-for-size when GROWI_OPTIMIZE_MEMORY=true', () => {
-    process.env.GROWI_OPTIMIZE_MEMORY = 'true';
+  it('should include --optimize-for-size when V8_OPTIMIZE_FOR_SIZE=true', () => {
+    process.env.V8_OPTIMIZE_FOR_SIZE = 'true';
     const flags = buildNodeFlags(undefined);
     const flags = buildNodeFlags(undefined);
     expect(flags).toContain('--optimize-for-size');
     expect(flags).toContain('--optimize-for-size');
   });
   });
 
 
-  it('should not include --optimize-for-size when GROWI_OPTIMIZE_MEMORY is not true', () => {
-    process.env.GROWI_OPTIMIZE_MEMORY = 'false';
+  it('should not include --optimize-for-size when V8_OPTIMIZE_FOR_SIZE is not true', () => {
+    process.env.V8_OPTIMIZE_FOR_SIZE = 'false';
     const flags = buildNodeFlags(undefined);
     const flags = buildNodeFlags(undefined);
     expect(flags).not.toContain('--optimize-for-size');
     expect(flags).not.toContain('--optimize-for-size');
   });
   });
 
 
-  it('should include --lite-mode when GROWI_LITE_MODE=true', () => {
-    process.env.GROWI_LITE_MODE = 'true';
+  it('should include --lite-mode when V8_LITE_MODE=true', () => {
+    process.env.V8_LITE_MODE = 'true';
     const flags = buildNodeFlags(undefined);
     const flags = buildNodeFlags(undefined);
     expect(flags).toContain('--lite-mode');
     expect(flags).toContain('--lite-mode');
   });
   });
 
 
-  it('should not include --lite-mode when GROWI_LITE_MODE is not true', () => {
-    delete process.env.GROWI_LITE_MODE;
+  it('should not include --lite-mode when V8_LITE_MODE is not true', () => {
+    delete process.env.V8_LITE_MODE;
     const flags = buildNodeFlags(undefined);
     const flags = buildNodeFlags(undefined);
     expect(flags).not.toContain('--lite-mode');
     expect(flags).not.toContain('--lite-mode');
   });
   });
 
 
   it('should combine all flags when all options enabled', () => {
   it('should combine all flags when all options enabled', () => {
-    process.env.GROWI_OPTIMIZE_MEMORY = 'true';
-    process.env.GROWI_LITE_MODE = 'true';
+    process.env.V8_OPTIMIZE_FOR_SIZE = 'true';
+    process.env.V8_LITE_MODE = 'true';
     const flags = buildNodeFlags(256);
     const flags = buildNodeFlags(256);
     expect(flags).toContain('--expose_gc');
     expect(flags).toContain('--expose_gc');
     expect(flags).toContain('--max-heap-size=256');
     expect(flags).toContain('--max-heap-size=256');

+ 10 - 10
apps/app/docker/docker-entrypoint.ts

@@ -6,7 +6,7 @@
  *
  *
  * Responsibilities:
  * Responsibilities:
  * - Directory setup (as root): /data/uploads, symlinks, /tmp/page-bulk-export
  * - Directory setup (as root): /data/uploads, symlinks, /tmp/page-bulk-export
- * - Heap size detection: GROWI_HEAP_SIZE → cgroup auto-calc → V8 default
+ * - Heap size detection: V8_MAX_HEAP_SIZE → cgroup auto-calc → V8 default
  * - Privilege drop: process.setgid + process.setuid (root → node)
  * - Privilege drop: process.setgid + process.setuid (root → node)
  * - Migration execution: execFileSync (no shell)
  * - Migration execution: execFileSync (no shell)
  * - App process spawn: spawn with signal forwarding
  * - App process spawn: spawn with signal forwarding
@@ -66,18 +66,18 @@ export function readCgroupLimit(filePath: string): number | undefined {
 
 
 /**
 /**
  * Detect heap size (MB) using 3-level fallback:
  * Detect heap size (MB) using 3-level fallback:
- * 1. GROWI_HEAP_SIZE env var
+ * 1. V8_MAX_HEAP_SIZE env var
  * 2. cgroup v2/v1 auto-calculation (60% of limit)
  * 2. cgroup v2/v1 auto-calculation (60% of limit)
  * 3. undefined (V8 default)
  * 3. undefined (V8 default)
  */
  */
 export function detectHeapSize(): number | undefined {
 export function detectHeapSize(): number | undefined {
-  // Priority 1: GROWI_HEAP_SIZE env
-  const envValue = process.env.GROWI_HEAP_SIZE;
+  // Priority 1: V8_MAX_HEAP_SIZE env
+  const envValue = process.env.V8_MAX_HEAP_SIZE;
   if (envValue != null && envValue !== '') {
   if (envValue != null && envValue !== '') {
     const parsed = parseInt(envValue, 10);
     const parsed = parseInt(envValue, 10);
     if (Number.isNaN(parsed)) {
     if (Number.isNaN(parsed)) {
       console.error(
       console.error(
-        `[entrypoint] GROWI_HEAP_SIZE="${envValue}" is not a valid number, ignoring`,
+        `[entrypoint] V8_MAX_HEAP_SIZE="${envValue}" is not a valid number, ignoring`,
       );
       );
       return undefined;
       return undefined;
     }
     }
@@ -110,11 +110,11 @@ export function buildNodeFlags(heapSize: number | undefined): string[] {
     flags.push(`--max-heap-size=${heapSize}`);
     flags.push(`--max-heap-size=${heapSize}`);
   }
   }
 
 
-  if (process.env.GROWI_OPTIMIZE_MEMORY === 'true') {
+  if (process.env.V8_OPTIMIZE_FOR_SIZE === 'true') {
     flags.push('--optimize-for-size');
     flags.push('--optimize-for-size');
   }
   }
 
 
-  if (process.env.GROWI_LITE_MODE === 'true') {
+  if (process.env.V8_LITE_MODE === 'true') {
     flags.push('--lite-mode');
     flags.push('--lite-mode');
   }
   }
 
 
@@ -163,10 +163,10 @@ export function dropPrivileges(): void {
 function logFlags(heapSize: number | undefined, flags: string[]): void {
 function logFlags(heapSize: number | undefined, flags: string[]): void {
   const source = (() => {
   const source = (() => {
     if (
     if (
-      process.env.GROWI_HEAP_SIZE != null &&
-      process.env.GROWI_HEAP_SIZE !== ''
+      process.env.V8_MAX_HEAP_SIZE != null &&
+      process.env.V8_MAX_HEAP_SIZE !== ''
     ) {
     ) {
-      return 'GROWI_HEAP_SIZE env';
+      return 'V8_MAX_HEAP_SIZE env';
     }
     }
     if (heapSize != null) return 'cgroup auto-detection';
     if (heapSize != null) return 'cgroup auto-detection';
     return 'V8 default (no heap limit)';
     return 'V8 default (no heap limit)';