vSphereCloud: add optional vCenter session keep-alive / connection pool#176
vSphereCloud: add optional vCenter session keep-alive / connection pool#176jimklimov wants to merge 4 commits into
Conversation
Every vSphere API call — find template, clone VM, configure, power on,
wait for tools, revert snapshot, delete clone — previously performed a
full login → operation → logout cycle. With multiple agents being
provisioned concurrently this produced dozens of authentication
round-trips per second against vCenter, increasing latency and load.
Three call sites additionally leaked sessions by never calling
disconnect() at all (vSphereCloudSlave.doTestConnection,
vSphereCloudSlaveComputer.getVMInformation,
vSphereCloudLauncher.revertVM).
A single long-lived vSphere session per vSphereCloud instance.
Callers continue to call vSphereInstance() / disconnect() exactly as
before; when the pool is active, disconnect() becomes a no-op and the
pool manages the real session lifecycle via background maintenance:
- Health check (configurable interval): issues a lightweight
currentTime() SOAP call; reconnects automatically on failure.
- Session age limit: proactively restarts the session after N seconds
to comply with vCenter session-duration policies.
- Use-count limit: restarts after N acquisitions, independently of age.
- Idle disconnect: logs out after N seconds of inactivity; the next
acquire() transparently re-establishes the session.
Each feature is independently enabled/disabled via its own integer
setting; 0 means the feature is disabled. The pool is disabled
entirely by default (useConnectionPool = false) to preserve the
existing stateless behaviour.
The pool holds ONE connection per cloud instance (not a pool of N
connections), because VSphere itself is stateless — its session token
is immutable and all operations create fresh ServiceInstance objects,
so concurrent callers share the same instance safely.
- volatile boolean pooled field: when true, disconnect() returns
immediately instead of calling logout().
- markAsPooled() / forceDisconnect(): package-private methods called
only by VSphereConnectionPool to control the real logout.
- isSessionAlive(): public; issues currentTime() and returns
true/false — used by the pool health-check thread.
Note: the correct yavijava 9.0.2 method name is currentTime(),
not getCurrentTime() which does not exist in this version.
Five new DataBoundSetter fields (all default 0 / false):
useConnectionPool boolean master toggle
poolHealthCheckIntervalSecs int seconds; 0 = no health checks
sessionMaxAgeSecs int seconds; 0 = never restart
sessionMaxUses int count; 0 = never restart
poolIdleTimeoutSecs int seconds; 0 = keep alive forever
vSphereInstance() routes through getOrCreatePool() when the toggle is
on. Each setter calls resetPool() so that live reconfiguration via
JCasC or the UI immediately takes effect with a fresh session.
- config.jelly: five new entries inside <f:advanced>
- help-useConnectionPool.html: overview + full JCasC YAML example
- help-poolHealthCheckIntervalSecs.html
- help-sessionMaxAgeSecs.html
- help-sessionMaxUses.html
- help-poolIdleTimeoutSecs.html
clouds:
- vSphere:
vsDescription: "My vCenter"
vsConnectionConfig:
vsHost: "https://vcenter.example.com"
credentialsId: "vcenter-creds"
useConnectionPool: true
poolHealthCheckIntervalSecs: 60 # 0 = disabled
sessionMaxAgeSecs: 3600 # 0 = never restart on age
sessionMaxUses: 500 # 0 = never restart on count
poolIdleTimeoutSecs: 300 # 0 = keep alive indefinitely
Three call sites that obtained a VSphere instance without ever calling
disconnect() have been fixed with try/finally blocks:
- vSphereCloudSlave.DescriptorImpl.doTestConnection() — was creating
up to two sessions (one for VM lookup, one for snapshot lookup)
and leaking both; refactored to a single session covering both
lookups.
- vSphereCloudSlaveComputer.getVMInformation() — leaked the session
after fetching VM properties for the computer detail panel.
- vSphereCloudLauncher.revertVM() — leaked the session used to
resolve the snapshot reference before reverting.
- VSphereConnectionPoolSettingsTest (9 unit tests, no Jenkins needed):
verifies pool is off by default, integer settings default to zero,
setter round-trips, pool shutdown is safe/idempotent, negative
constructor arguments are clamped to zero.
- configuration-as-code-with-pool.yml: new JCasC fixture with all
five pool fields set to non-default values.
- ConfigurationAsCodeTest: two new methods —
pool_is_disabled_by_default_when_not_specified_in_yaml and
should_load_pool_configuration_from_yaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jim Klimov <jimklimov+jenkinsci@gmail.com>
The ${%...} i18n syntax routes the key through Apache JEXL, which
parses parentheses as a function call and hyphens as arithmetic.
Titles such as "${%Pool health-check interval (seconds)}" therefore
failed to parse at startup with:
Unable to create expression: connection pool
Encountered "pool" at line 1, column 12.
Fix: rephrase the five new keys to avoid both constructs so that
${%...} can be used and future translations remain possible:
"Use vSphere connection pool"
"Pool health check interval in seconds"
"Pool session max age in seconds"
"Pool session max uses"
"Pool idle disconnect timeout in seconds"
Also replace the em-dash characters in the description attributes
with plain ASCII hyphens.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jim Klimov <jimklimov+jenkinsci@gmail.com>
Signed-off-by: Jim Klimov <jimklimov+jenkinsci@gmail.com>
|
Ran some experiments with this PR's result, seems promising:
Some disappointments were also there: I've initially tried to enable several options at once:
With these settings, it reconnected every 70-75 seconds (not 60), consuming about 20 API operations during each session even if there were no requests from the pipelines. It never went into idle disconnect handling.
I then toned down the settings to:
However it seems to have been ignored during the continuing lifetime of the Jenkins server. While there were logged
Ultimately I've restarted the Jenkins instance, and it seemed to have the desired effect:
Again, the requested idle time (120s) was exceeded, but at least did work this time. There was a logout event in vCenter log, and there were no subsequent interactions. One nuance that needs more attention here - when Jenkins was being restarted, there was no logout from vCenter. However as the Jenkins JVM was SIGTERM'ed and by itself just died and did not log any graceful teardown log lines, I can't say yet whether the plugin handles it cleanly or not when all goes well. |
|
Continuing with the experiment, I've updated the configuration with a health-check again:
Again, in Jenkins log the "STARTING VSPHERE CLOUD" line appeared and nothing else per se. Only after I re-ran the test job, it added "session established" at 14:49:09, and "disconnecting - idle for 143s (limit 120s)" at 14:51:38. This was without the Jenkins controller restart (so maybe the plugin effectively ignored the config change). A subsequent |
|
Retested with starting a job (and so a vCenter session) and subsequent
|
|
Retrying with shorter-lived settings:
Ran the test job, "session established" at 15:00:57, last operation in vCenter logged at 15:01:04 and job finished at 15:01:09. Notably, the settings were modified and applied (logging "STARTING VSPHERE CLOUD") after starting Jenkins but before running the job, and took hold in the same server uptime. The session ended on vCenter side of the log too, with 40 "API invocations" vs. 52 at 14:58:38 or 22 at 14:24:23 with earlier tests. No subsequent re-login logged - by neither vCenter nor Jenkins. |
|
Updated the above settings to poll every 20 seconds, restarted Jenkins and initiated the test job right away (without any visit to the cloud config page in this new uptime). This time there is no "STARTING VSPHERE CLOUD" in the log at all; "session established" at 15:11:31, job ended at 15:11:43, and "disconnecting - idle for 83s (limit 60s)" at 15:13:00 with 30 logged API calls (so apparently health-check polling counts among those, probably). No subsequent re-login. So for the idle time vs. session max time (and probably max operations per session), we need to be careful about accounting the last request from plugin consumers, and ignore the (sub-)session start time - if it is not the first loop cycle. |
|
Modified the config to check also API usage (max ops per session):
As before, this change did not take hold in the same Jenkins uptime (since a job was executed). After a restart of Jenkins, started the test job right away.
|
|
Retry without max age, only max usage:
Apply, restart,.. but before I re-ran there was some delay due to a coffee break - and vCenter logged that "user ... logged out (login time: Tuesday, July 14, 2026 4:55:51 PM CEST, number of API invocations: 29, user agent: Java/25.0.2)" at "07/14/2026, 5:26:40 PM" (local time). This probably corresponds to earlier test with
Re-running the test job again did not interrupt sessions due to API invocations, plugin disconnected itself some exactly 90 seconds (idle 83 of 60) after establishing the session, consuming 28 API invocations (probably one less health-check than before with 30 seen several times).
UPDATE: Retrying with extreme low value of "Pool session max uses: 2" to check whether it is not the case of the job does not fit into the allowance while polling or whatever is not accounted into it. And bumped health check interval to 35. Hooray:
Not looking at code, I suppose this session constraint check happens then a plugin operation starts, so code proceeds atomically in same session, regardless of the API consumption (which is probably a good thing so it is not broken mid-way). The next operation may find that the allowance is overspent, and would start another session.
|
|
At some point I've checked that running a job, letting the session expire by timeout (or now also call count), and starting another in the same Jenkins uptime is seamless, jobs succeed. What about running several jobs while the same session is up? With same uptime as the previous test, but some 17 minutes after that ended and I was documenting the results, I've gone to edit cloud config to disable the call count constraint. Either the edit worked without Jenkins restart, or the constraint only works once, but subsequent job runs did not reconnect due to "max uses".
|
Augmented by AI (Claude)
Every vSphere API call — find template, clone VM, configure, power on, wait for tools, revert snapshot, delete clone — previously performed a full login → operation → logout cycle. With multiple agents being provisioned concurrently this produced dozens of authentication round-trips per second against vCenter, increasing latency and load.
Three call sites additionally leaked sessions by never calling disconnect() at all:
A single long-lived vSphere session per vSphereCloud instance. Callers continue to call vSphereInstance() / disconnect() exactly as before; when the pool is active, disconnect() becomes a no-op and the pool manages the real session lifecycle via background maintenance:
Each feature is independently enabled/disabled via its own integer setting; 0 means the feature is disabled. The pool is disabled entirely by default (useConnectionPool = false) to preserve the existing stateless behaviour.
The pool holds ONE connection per cloud instance (not a pool of N connections), because VSphere itself is stateless — its session token is immutable and all operations create fresh ServiceInstance objects, so concurrent callers share the same instance safely.
Five new DataBoundSetter fields (all default 0 / false):
| useConnectionPool | boolean | master toggle |
| poolHealthCheckIntervalSecs | int | seconds; 0 = no health checks |
| sessionMaxAgeSecs | int | seconds; 0 = never restart |
| sessionMaxUses | int | count; 0 = never restart |
| poolIdleTimeoutSecs | int | seconds; 0 = keep alive forever |
vSphereInstance() routes through getOrCreatePool() when the toggle is on. Each setter calls resetPool() so that live reconfiguration via JCasC or the UI immediately takes effect with a fresh session.
clouds:
Three call sites that obtained a VSphere instance without ever calling disconnect() have been fixed with try/finally blocks:
vSphereCloudSlave.DescriptorImpl.doTestConnection() — was creating up to two sessions (one for VM lookup, one for snapshot lookup) and leaking both; refactored to a single session covering both lookups.
vSphereCloudSlaveComputer.getVMInformation() — leaked the session after fetching VM properties for the computer detail panel.
vSphereCloudLauncher.revertVM() — leaked the session used to resolve the snapshot reference before reverting.
VSphereConnectionPoolSettingsTest (9 unit tests, no Jenkins needed): verifies pool is off by default, integer settings default to zero, setter round-trips, pool shutdown is safe/idempotent, negative constructor arguments are clamped to zero.
configuration-as-code-with-pool.yml: new JCasC fixture with all five pool fields set to non-default values.
ConfigurationAsCodeTest: two new methods — pool_is_disabled_by_default_when_not_specified_in_yaml and should_load_pool_configuration_from_yaml.
Testing done
Will be tested in-house in the coming weeks/months.
Submitter checklist