Menachem Shapira created GUACAMOLE-2316:
-------------------------------------------

             Summary: Allow the number of display encoder worker threads per 
connection to be limited
                 Key: GUACAMOLE-2316
                 URL: https://issues.apache.org/jira/browse/GUACAMOLE-2316
             Project: Guacamole
          Issue Type: Improvement
          Components: guacd, libguac
    Affects Versions: 1.6.1 [staging]
            Reporter: Menachem Shapira


*Problem*

{{guac_display_alloc()}} sizes its encoder worker thread pool from the number 
of available processors:

{{display->worker_thread_count = cpu_count * GUAC_DISPLAY_CPU_THREAD_FACTOR;}}
Each connection gets its own {{{}guac_display{}}}, so the total number of 
encoder threads in a guacd deployment grows as {{{}concurrent_connections × 
available_processors{}}}. There is no upper bound.

On modern high-core-count servers this scales badly. On a 128-core host, 50 
concurrent connections request on the order of 6,400 encoder threads. Well 
before that, the host hits per-process/per-user thread limits 
({{{}RLIMIT_NPROC{}}}, {{pids.max}} in containers, {{{}threads-max{}}}), thread 
stack memory, and scheduler overhead. The failure is not graceful: 
{{pthread_create()}} return values are not currently checked, so exhaustion 
surfaces as unpredictable behavior rather than a clean error.

The parallelism is also largely wasted. A single display rarely produces enough 
concurrently-encodable work to occupy that many processors, so the marginal 
thread contributes contention rather than throughput.

*Proposal*

Make the per-connection encoder worker thread count configurable, with the 
limit owned by the {{guac_client}} and read at display allocation time.
 * Add {{int max_display_worker_threads}} to {{guac_client}} (appended to the 
struct; {{0}} = no limit beyond available processors, the current behavior).
 * {{guac_display_alloc()}} caps its computed worker count against the owning 
client's value. The cap can only reduce the count, never increase it, and is 
applied before the worker array is allocated and before any 
{{pthread_create()}} call.
 * guacd exposes the limit as {{max_worker_threads}} in {{guacd.conf}} and 
{{-w}} on the command line, carries it through connection routing, and assigns 
it to {{proc->client}} in {{guacd_create_proc()}} before {{{}fork(){}}}.
 * The guacd Docker image exposes it as the {{MAX_WORKER_THREADS}} environment 
variable.

Storing the limit on the client rather than in process-global libguac state 
means the value transports naturally across the guacd → protocol-plugin 
boundary (guacd does not itself call {{{}guac_display_alloc(){}}}; the RDP/VNC 
implementations do), independent clients in one process can use different 
limits, there is no non-thread-safe public setter and no data race, and no 
existing public signature changes.

*Scope / non-goals*

This is a *per-connection* cap, not a daemon-wide thread budget. It changes 
growth from {{connections × detected_cpus}} to {{{}connections × 
configured_limit{}}}; it does not impose a hard ceiling on total encoder 
threads across all connections. A true global budget would require a shared 
worker pool or admission control and is a separate, substantially larger design.

Also out of scope: resizing an existing display's worker pool at runtime, and 
configuration reload.

*Open decision: default value*

Two defensible policies, and this should be an explicit maintainer decision 
rather than an implicit one:
 * _Compatibility-first_ (what the current patch does): default {{0}} / 
unlimited. Zero regression risk, but the hazard remains for every deployment 
that doesn't discover the setting — the change is a mitigation operators must 
opt into, not a fix.
 * {_}Safety-first{_}: a small finite default (e.g. 4), with {{0}} explicitly 
opting back into legacy CPU-count behavior. Fixes existing deployments on 
upgrade, but needs benchmark evidence and release notes since some workloads 
may regress.

Suggest benchmarking effective counts of 1, 2, 4, 8, and CPU-count across 
representative RDP/VNC workloads (typical resolutions, high-motion content) 
under both single-connection and high-concurrency load before choosing.

*Acceptance criteria*
 # {{guacd.conf}} {{max_worker_threads}} and {{-w}} accept a non-negative 
integer; non-numeric, trailing-garbage, negative, and out-of-range values are 
rejected with a clear error at startup.
 # A configured limit lower than the detected processor count produces exactly 
that many worker threads per display.
 # A configured limit higher than the detected count does not increase the 
thread count above the CPU-derived value.
 # A limit of {{1}} produces exactly one worker thread.
 # {{0}} (and unset) preserves current behavior exactly.
 # Two clients in one process with different limits produce different worker 
counts; a configured client does not affect a client using the default.
 # The limit is read at {{guac_display_alloc()}} time; changing it afterward 
does not mutate an already-allocated display.
 # A libguac consumer that never sets the field gets the default — verify 
{{guac_client_alloc()}} zero-initializes it.
 # Man pages ({{{}guacd.8{}}}, {{{}guacd.conf.5{}}}) document that the limit is 
per connection, not daemon-wide.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to