@@ -3,7 +3,7 @@ import fs from 'fs';
33import os from 'os' ;
44import path from 'path' ;
55import { PassThrough } from 'stream' ;
6- import { SITE_RUNTIME_NATIVE_PHP } from '@studio/common/lib/site-runtime' ;
6+ import { SITE_RUNTIME_NATIVE_PHP , SITE_RUNTIME_PLAYGROUND } from '@studio/common/lib/site-runtime' ;
77import { beforeEach , describe , expect , it , vi } from 'vitest' ;
88
99const testProcessName = 'studio-site-process-manager-test' ;
@@ -297,6 +297,129 @@ describe( 'ProcessManagerDaemon', () => {
297297 }
298298 ) ;
299299
300+ describe ( 'weighted capacity limit' , ( ) => {
301+ type HandleRequestFn = ( request : unknown ) => Promise < {
302+ type : string ;
303+ payload : { process ?: { pmId : number ; name : string ; status : string ; pid ?: number } } ;
304+ } > ;
305+
306+ async function startSiteProcess (
307+ handleRequest : HandleRequestFn ,
308+ index : number ,
309+ runtime : string = SITE_RUNTIME_NATIVE_PHP
310+ ) {
311+ return handleRequest ( {
312+ type : 'start-process' ,
313+ requestId : String ( index ) ,
314+ processName : `studio-site-site-${ index } ` ,
315+ scriptPath : '/tmp/test-child.js' ,
316+ env : { } ,
317+ args : [ ] ,
318+ runtime,
319+ } ) ;
320+ }
321+
322+ it ( 'rejects a site process when the weighted capacity limit is exceeded' , async ( ) => {
323+ spawnMock . mockImplementation ( ( ) => new MockChildProcess ( ) ) ;
324+ const { ProcessManagerDaemon } = await import ( '../process-manager-daemon' ) ;
325+
326+ const daemon = new ProcessManagerDaemon ( ) ;
327+ const handleRequest = (
328+ daemon as unknown as { handleRequest : HandleRequestFn }
329+ ) . handleRequest . bind ( daemon ) ;
330+ vi . spyOn (
331+ daemon as unknown as { broadcastEvent : ( event : unknown ) => Promise < void > } ,
332+ 'broadcastEvent'
333+ ) . mockResolvedValue ( undefined ) ;
334+
335+ // Start 36 native-php sites (weight 1 each = 36 total, at the limit)
336+ for ( let i = 0 ; i < 36 ; i ++ ) {
337+ await startSiteProcess ( handleRequest , i ) ;
338+ }
339+
340+ // The 37th native-php site should be rejected
341+ await expect ( startSiteProcess ( handleRequest , 37 ) ) . rejects . toThrow (
342+ 'CAPACITY_LIMIT_REACHED'
343+ ) ;
344+ } ) ;
345+
346+ it ( 'counts playground sites with weight 6' , async ( ) => {
347+ spawnMock . mockImplementation ( ( ) => new MockChildProcess ( ) ) ;
348+ const { ProcessManagerDaemon } = await import ( '../process-manager-daemon' ) ;
349+
350+ const daemon = new ProcessManagerDaemon ( ) ;
351+ const handleRequest = (
352+ daemon as unknown as { handleRequest : HandleRequestFn }
353+ ) . handleRequest . bind ( daemon ) ;
354+ vi . spyOn (
355+ daemon as unknown as { broadcastEvent : ( event : unknown ) => Promise < void > } ,
356+ 'broadcastEvent'
357+ ) . mockResolvedValue ( undefined ) ;
358+
359+ // Start 6 playground sites (weight 6 each = 36 total, at the limit)
360+ for ( let i = 0 ; i < 6 ; i ++ ) {
361+ await startSiteProcess ( handleRequest , i , SITE_RUNTIME_PLAYGROUND ) ;
362+ }
363+
364+ // The 7th playground site should be rejected
365+ await expect ( startSiteProcess ( handleRequest , 7 , SITE_RUNTIME_PLAYGROUND ) ) . rejects . toThrow (
366+ 'CAPACITY_LIMIT_REACHED'
367+ ) ;
368+ } ) ;
369+
370+ it ( 'does not count non-site processes toward capacity' , async ( ) => {
371+ spawnMock . mockImplementation ( ( ) => new MockChildProcess ( ) ) ;
372+ const { ProcessManagerDaemon } = await import ( '../process-manager-daemon' ) ;
373+
374+ const daemon = new ProcessManagerDaemon ( ) ;
375+ const handleRequest = (
376+ daemon as unknown as { handleRequest : HandleRequestFn }
377+ ) . handleRequest . bind ( daemon ) ;
378+ vi . spyOn (
379+ daemon as unknown as { broadcastEvent : ( event : unknown ) => Promise < void > } ,
380+ 'broadcastEvent'
381+ ) . mockResolvedValue ( undefined ) ;
382+
383+ // Start a non-site process (e.g. proxy)
384+ await handleRequest ( {
385+ type : 'start-process' ,
386+ requestId : '0' ,
387+ processName : 'studio-proxy' ,
388+ scriptPath : '/tmp/proxy.js' ,
389+ env : { } ,
390+ args : [ ] ,
391+ } ) ;
392+
393+ // Should still be able to start 36 native-php site processes
394+ for ( let i = 0 ; i < 36 ; i ++ ) {
395+ await startSiteProcess ( handleRequest , i ) ;
396+ }
397+ } ) ;
398+
399+ it ( 'allows restarting an already-running process regardless of capacity' , async ( ) => {
400+ spawnMock . mockImplementation ( ( ) => new MockChildProcess ( ) ) ;
401+ const { ProcessManagerDaemon } = await import ( '../process-manager-daemon' ) ;
402+
403+ const daemon = new ProcessManagerDaemon ( ) ;
404+ const handleRequest = (
405+ daemon as unknown as { handleRequest : HandleRequestFn }
406+ ) . handleRequest . bind ( daemon ) ;
407+ vi . spyOn (
408+ daemon as unknown as { broadcastEvent : ( event : unknown ) => Promise < void > } ,
409+ 'broadcastEvent'
410+ ) . mockResolvedValue ( undefined ) ;
411+
412+ // Fill capacity
413+ for ( let i = 0 ; i < 36 ; i ++ ) {
414+ await startSiteProcess ( handleRequest , i ) ;
415+ }
416+
417+ // Re-starting an existing process should succeed (early return, no capacity check)
418+ const result = await startSiteProcess ( handleRequest , 0 ) ;
419+ expect ( result . payload . process ?. status ) . toBe ( 'online' ) ;
420+ } ) ;
421+ } ) ;
422+
300423 it ( 'taskkills the wrapper and reported subprocess trees on Windows' , async ( ) => {
301424 const child = new MockChildProcess ( ) ;
302425 spawnMock . mockReturnValue ( child ) ;
0 commit comments