Skip to content

Commit 73e5075

Browse files
pditommasojorgee
andauthored
Add time-based caching for K8sConfig.getClient() (nextflow-io#6742)
Co-authored-by: Jorge Ejarque <jorgee@users.noreply.github.com>
1 parent 429b776 commit 73e5075

3 files changed

Lines changed: 71 additions & 2 deletions

File tree

docs/reference/config.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,12 @@ The following settings are available:
10571057
- `clientKey`
10581058
- `clientKeyFile`
10591059

1060+
`k8s.clientRefreshInterval`
1061+
: :::{versionadded} 26.01.0-edge
1062+
:::
1063+
: The interval after which the Kubernetes client configuration is refreshed (default: `50m`).
1064+
: This setting is useful when the Kubernetes authentication token has a limited lifespan and needs to be periodically refreshed. The client configuration will be automatically reloaded after the specified interval, allowing Nextflow to obtain fresh credentials from the Kubernetes configuration.
1065+
10601066
`k8s.computeResourceType`
10611067
: :::{versionadded} 22.05.0-edge
10621068
:::

plugins/nf-k8s/src/main/nextflow/k8s/K8sConfig.groovy

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ package nextflow.k8s
1818

1919
import nextflow.k8s.client.K8sRetryConfig
2020

21+
import java.util.concurrent.TimeUnit
2122
import javax.annotation.Nullable
2223

24+
import com.google.common.cache.Cache
25+
import com.google.common.cache.CacheBuilder
2326
import groovy.transform.CompileStatic
24-
import groovy.transform.Memoized
2527
import groovy.transform.PackageScope
2628
import groovy.util.logging.Slf4j
2729
import nextflow.BuildInfo
@@ -55,6 +57,8 @@ class K8sConfig implements ConfigScope {
5557

5658
static final private Map<String,?> DEFAULT_FUSE_PLUGIN = Map.of('nextflow.io/fuse', 1)
5759

60+
private Cache<String, ClientConfig> clientCache
61+
5862
@ConfigOption
5963
@Description("""
6064
Automatically mount host paths into the task pods (default: `false`). Only intended for development purposes when using a single node.
@@ -81,6 +85,12 @@ class K8sConfig implements ConfigScope {
8185
""")
8286
final Map client
8387

88+
@ConfigOption
89+
@Description("""
90+
The interval after which the Kubernetes client configuration is refreshed (default: `50m`).
91+
""")
92+
final Duration clientRefreshInterval
93+
8494
@ConfigOption
8595
@Description("""
8696
The Kubernetes [configuration context](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) to use.
@@ -215,6 +225,10 @@ class K8sConfig implements ConfigScope {
215225
autoMountHostPaths = opts.autoMountHostPaths as boolean
216226
cleanup = opts.cleanup as Boolean
217227
client = opts.client as Map
228+
clientRefreshInterval = opts.clientRefreshInterval as Duration ?: Duration.of('50m')
229+
clientCache = CacheBuilder.newBuilder()
230+
.expireAfterWrite(clientRefreshInterval.toMillis(), TimeUnit.MILLISECONDS)
231+
.build()
218232
computeResourceType = opts.computeResourceType
219233
context = opts.context
220234
cpuLimits = opts.cpuLimits as boolean
@@ -351,9 +365,11 @@ class K8sConfig implements ConfigScope {
351365
return result ? result.claimName : null
352366
}
353367

354-
@Memoized
355368
ClientConfig getClient() {
369+
return clientCache.get('client', this::getClient0)
370+
}
356371

372+
private ClientConfig getClient0() {
357373
final result = client != null
358374
? clientFromNextflow(client, namespace, serviceAccount)
359375
: clientDiscovery(context, namespace, serviceAccount)

plugins/nf-k8s/src/test/nextflow/k8s/K8sConfigTest.groovy

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,4 +466,51 @@ class K8sConfigTest extends Specification {
466466
then:
467467
cfg.fetchNodeName() == false
468468
}
469+
470+
def 'should set clientRefreshInterval' () {
471+
when:
472+
def cfg = new K8sConfig()
473+
then:
474+
cfg.clientRefreshInterval == Duration.of('50m')
475+
476+
when:
477+
cfg = new K8sConfig(clientRefreshInterval: '30m')
478+
then:
479+
cfg.clientRefreshInterval == Duration.of('30m')
480+
481+
when:
482+
cfg = new K8sConfig(clientRefreshInterval: '1h')
483+
then:
484+
cfg.clientRefreshInterval == Duration.of('1h')
485+
}
486+
487+
def 'should cache client config and refresh after expiration' () {
488+
given:
489+
def CONFIG = [
490+
namespace: 'test-ns',
491+
serviceAccount: 'test-sa',
492+
client: [server: 'http://k8s-server'],
493+
clientRefreshInterval: '100ms'
494+
]
495+
K8sConfig config = Spy(K8sConfig, constructorArgs: [CONFIG])
496+
497+
when: 'first call to getClient'
498+
def client1 = config.getClient()
499+
then: 'client is created via clientFromNextflow'
500+
1 * config.clientFromNextflow(_, _, _) >> new ClientConfig(server: 'http://k8s-server', namespace: 'test-ns')
501+
client1.server == 'http://k8s-server'
502+
503+
when: 'second call within cache interval'
504+
def client2 = config.getClient()
505+
then: 'returns cached client without calling clientFromNextflow again'
506+
0 * config.clientFromNextflow(_, _, _)
507+
client2.is(client1)
508+
509+
when: 'call after cache expiration'
510+
sleep(150) // wait for cache to expire
511+
def client3 = config.getClient()
512+
then: 'client is recreated'
513+
1 * config.clientFromNextflow(_, _, _) >> new ClientConfig(server: 'http://k8s-server', namespace: 'test-ns')
514+
!client3.is(client1)
515+
}
469516
}

0 commit comments

Comments
 (0)