Skip to content

Commit 9c78d24

Browse files
thjaeckleclaude
andcommitted
Support hot-reloading of Ditto HOCON configuration via Kubernetes ConfigMap file watching
Adds a DynamicConfigWatcherActor that polls a ConfigMap-mounted file for changes, merges dynamic config with the static config, and notifies consumers via two patterns: - EventStream-based DynamicConfigChanged events for singleton/low-count actors - DynamicConfigPoller utility for high-volume sharded actors (version-polling pattern) Config values that can now be changed without pod restart include namespace-scoped activity checks, custom metrics, entity creation restrictions, gateway authentication (OAuth issuers, DevOps auth, pre-authentication), streaming config, background sync, persistence cleanup, and WoT ThingModel validation. Includes Helm chart changes to support a separate dynamic ConfigMap (without checksum annotation, so updates don't trigger pod restarts) and version-keyed parsed config caching in DynamicConfigWatcherExtension to minimize memory overhead across 500K+ sharded actor instances. Closes: #2366 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent afabcfb commit 9c78d24

43 files changed

Lines changed: 1937 additions & 158 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

base/service/src/main/java/org/eclipse/ditto/base/service/DittoService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.eclipse.ditto.base.service.config.json.JsonConfig;
4242
import org.eclipse.ditto.base.service.devops.DevOpsCommandsActor;
4343
import org.eclipse.ditto.base.service.devops.LogbackLoggingFacade;
44+
import org.eclipse.ditto.internal.utils.pekko.config.DynamicConfigWatcherExtension;
4445
import org.eclipse.ditto.internal.utils.config.DefaultScopedConfig;
4546
import org.eclipse.ditto.internal.utils.config.DittoConfigError;
4647
import org.eclipse.ditto.internal.utils.config.InstanceIdentifierSupplier;
@@ -271,6 +272,7 @@ private void startPrometheusReporter() {
271272
private void initializeActorSystem(final ActorSystem actorSystem) {
272273
startPekkoManagement(actorSystem);
273274
startClusterBootstrap(actorSystem);
275+
DynamicConfigWatcherExtension.get(actorSystem);
274276

275277
startStatusSupplierActor(actorSystem);
276278
startDevOpsCommandsActor(actorSystem);

connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/ConnectivityRootActor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ private ConnectivityRootActor(final ConnectivityConfig connectivityConfig,
129129

130130

131131
final var cleanupConfig = connectivityConfig.getConnectionConfig().getCleanupConfig();
132-
final var cleanupActorProps = PersistenceCleanupActor.props(cleanupConfig, mongoReadJournal, CLUSTER_ROLE);
132+
final var cleanupActorProps = PersistenceCleanupActor.props(cleanupConfig, mongoReadJournal, CLUSTER_ROLE,
133+
"ditto.connectivity.connection.cleanup");
133134
startChildActor(PersistenceCleanupActor.ACTOR_NAME, cleanupActorProps);
134135

135136
final ActorRef healthCheckingActor = getHealthCheckingActor(connectivityConfig);

deployment/helm/ditto/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ description: |
1616
A digital twin is a virtual, cloud based, representation of his real world counterpart
1717
(real world “Things”, e.g. devices like sensors, smart heating, connected cars, smart grids, EV charging stations etc).
1818
type: application
19-
version: 3.8.16 # chart version is effectively set by release-job
19+
version: 3.8.17 # chart version is effectively set by release-job
2020
appVersion: 3.8.12
2121
keywords:
2222
- iot-chart

deployment/helm/ditto/templates/connectivity-deployment.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,14 @@ spec:
374374
value: "{{ .Values.connectivity.config.connections.encryption.migration.batchSize }}"
375375
- name: CONNECTIVITY_ENCRYPTION_MIGRATION_MAX_DOCS_PER_MINUTE
376376
value: "{{ .Values.connectivity.config.connections.encryption.migration.maxDocumentsPerMinute }}"
377+
{{- if .Values.connectivity.dynamicConfig.enabled }}
378+
- name: DITTO_DYNAMIC_CONFIG_ENABLED
379+
value: "true"
380+
- name: DITTO_DYNAMIC_CONFIG_FILE_PATH
381+
value: "{{ .Values.connectivity.dynamicConfig.filePath }}"
382+
- name: DITTO_DYNAMIC_CONFIG_POLL_INTERVAL
383+
value: "{{ .Values.connectivity.dynamicConfig.pollInterval }}"
384+
{{- end }}
377385
{{- if .Values.connectivity.extraEnv }}
378386
{{- toYaml .Values.connectivity.extraEnv | nindent 12 }}
379387
{{- end }}
@@ -447,6 +455,11 @@ spec:
447455
{{- end }}
448456
- name: ditto-heap-dumps
449457
mountPath: /opt/ditto/dumps
458+
{{- if .Values.connectivity.dynamicConfig.enabled }}
459+
- name: ditto-dynamic-config
460+
mountPath: {{ dir .Values.connectivity.dynamicConfig.filePath }}
461+
readOnly: true
462+
{{- end }}
450463
resources:
451464
requests:
452465
cpu: {{ mulf .Values.connectivity.resources.cpu 1000 }}m
@@ -503,4 +516,9 @@ spec:
503516
{{- end }}
504517
- name: ditto-heap-dumps
505518
emptyDir: {}
519+
{{- if .Values.connectivity.dynamicConfig.enabled }}
520+
- name: ditto-dynamic-config
521+
configMap:
522+
name: {{ .Release.Name }}-connectivity-dynamic-config
523+
{{- end }}
506524
{{- end }}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
2+
#
3+
# See the NOTICE file(s) distributed with this work for additional
4+
# information regarding copyright ownership.
5+
#
6+
# This program and the accompanying materials are made available under the
7+
# terms of the Eclipse Public License 2.0 which is available at
8+
# http://www.eclipse.org/legal/epl-2.0
9+
#
10+
# SPDX-License-Identifier: EPL-2.0
11+
{{- $releaseName := .Release.Name -}}
12+
{{- $name := include "ditto.name" . -}}
13+
{{- $labels := include "ditto.labels" . -}}
14+
{{- $namespace := .Release.Namespace -}}
15+
{{- range $serviceName, $serviceValues := dict "policies" .Values.policies "things" .Values.things "thingsSearch" .Values.thingsSearch "connectivity" .Values.connectivity "gateway" .Values.gateway }}
16+
{{- if $serviceValues.dynamicConfig.enabled }}
17+
---
18+
apiVersion: v1
19+
kind: ConfigMap
20+
metadata:
21+
name: {{ $releaseName }}-{{ $serviceName | kebabcase }}-dynamic-config
22+
namespace: {{ $namespace }}
23+
labels:
24+
app.kubernetes.io/name: {{ $name }}-{{ $serviceName | kebabcase }}-dynamic-config
25+
{{ $labels | indent 4 }}
26+
data:
27+
dynamic.conf: |-
28+
{{- if $serviceValues.dynamicConfig.content }}
29+
{{ $serviceValues.dynamicConfig.content | indent 4 }}
30+
{{- end }}
31+
{{- end }}
32+
{{- end }}

deployment/helm/ditto/templates/gateway-deployment.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,14 @@ spec:
279279
value: "{{ .Values.gateway.config.wotDirectory.basePrefix }}"
280280
- name: GATEWAY_WOT_DIRECTORY_AUTHENTICATION_REQUIRED
281281
value: "{{ .Values.gateway.config.wotDirectory.authenticationRequired }}"
282+
{{- if .Values.gateway.dynamicConfig.enabled }}
283+
- name: DITTO_DYNAMIC_CONFIG_ENABLED
284+
value: "true"
285+
- name: DITTO_DYNAMIC_CONFIG_FILE_PATH
286+
value: "{{ .Values.gateway.dynamicConfig.filePath }}"
287+
- name: DITTO_DYNAMIC_CONFIG_POLL_INTERVAL
288+
value: "{{ .Values.gateway.dynamicConfig.pollInterval }}"
289+
{{- end }}
282290
{{- if .Values.gateway.extraEnv }}
283291
{{- toYaml .Values.gateway.extraEnv | nindent 12 }}
284292
{{- end }}
@@ -341,6 +349,11 @@ spec:
341349
{{- end }}
342350
- name: ditto-heap-dumps
343351
mountPath: /opt/ditto/dumps
352+
{{- if .Values.gateway.dynamicConfig.enabled }}
353+
- name: ditto-dynamic-config
354+
mountPath: {{ dir .Values.gateway.dynamicConfig.filePath }}
355+
readOnly: true
356+
{{- end }}
344357
resources:
345358
requests:
346359
cpu: {{ mulf .Values.gateway.resources.cpu 1000 }}m
@@ -397,4 +410,9 @@ spec:
397410
{{- end }}
398411
- name: ditto-heap-dumps
399412
emptyDir: {}
413+
{{- if .Values.gateway.dynamicConfig.enabled }}
414+
- name: ditto-dynamic-config
415+
configMap:
416+
name: {{ .Release.Name }}-gateway-dynamic-config
417+
{{- end }}
400418
{{- end }}

deployment/helm/ditto/templates/policies-deployment.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,14 @@ spec:
314314
value: "{{ .Values.policies.config.policiesEnforcer.cache.expireAfterWrite }}"
315315
- name: DITTO_POLICIES_ENFORCER_CACHE_EXPIRE_AFTER_ACCESS
316316
value: "{{ .Values.policies.config.policiesEnforcer.cache.expireAfterAccess }}"
317+
{{- if .Values.policies.dynamicConfig.enabled }}
318+
- name: DITTO_DYNAMIC_CONFIG_ENABLED
319+
value: "true"
320+
- name: DITTO_DYNAMIC_CONFIG_FILE_PATH
321+
value: "{{ .Values.policies.dynamicConfig.filePath }}"
322+
- name: DITTO_DYNAMIC_CONFIG_POLL_INTERVAL
323+
value: "{{ .Values.policies.dynamicConfig.pollInterval }}"
324+
{{- end }}
317325
{{- if .Values.policies.extraEnv }}
318326
{{- toYaml .Values.policies.extraEnv | nindent 12 }}
319327
{{- end }}
@@ -390,6 +398,11 @@ spec:
390398
{{- end }}
391399
- name: ditto-heap-dumps
392400
mountPath: /opt/ditto/dumps
401+
{{- if .Values.policies.dynamicConfig.enabled }}
402+
- name: ditto-dynamic-config
403+
mountPath: {{ dir .Values.policies.dynamicConfig.filePath }}
404+
readOnly: true
405+
{{- end }}
393406
resources:
394407
requests:
395408
cpu: {{ mulf .Values.policies.resources.cpu 1000 }}m
@@ -446,4 +459,9 @@ spec:
446459
{{- end }}
447460
- name: ditto-heap-dumps
448461
emptyDir: {}
462+
{{- if .Values.policies.dynamicConfig.enabled }}
463+
- name: ditto-dynamic-config
464+
configMap:
465+
name: {{ .Release.Name }}-policies-dynamic-config
466+
{{- end }}
449467
{{- end }}

deployment/helm/ditto/templates/things-deployment.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,14 @@ spec:
386386
value: "{{ index .Values.things.config.wot.tmValidation.feature.forbid "non-modeled-desired-properties" }}"
387387
- name: THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_FORBID_NON_MODELED_OUTBOX_MESSAGES
388388
value: "{{ index .Values.things.config.wot.tmValidation.feature.forbid "non-modeled-outbox-messages" }}"
389+
{{- if .Values.things.dynamicConfig.enabled }}
390+
- name: DITTO_DYNAMIC_CONFIG_ENABLED
391+
value: "true"
392+
- name: DITTO_DYNAMIC_CONFIG_FILE_PATH
393+
value: "{{ .Values.things.dynamicConfig.filePath }}"
394+
- name: DITTO_DYNAMIC_CONFIG_POLL_INTERVAL
395+
value: "{{ .Values.things.dynamicConfig.pollInterval }}"
396+
{{- end }}
389397
{{- if .Values.things.extraEnv }}
390398
{{- toYaml .Values.things.extraEnv | nindent 12 }}
391399
{{- end }}
@@ -459,6 +467,11 @@ spec:
459467
{{- end }}
460468
- name: ditto-heap-dumps
461469
mountPath: /opt/ditto/dumps
470+
{{- if .Values.things.dynamicConfig.enabled }}
471+
- name: ditto-dynamic-config
472+
mountPath: {{ dir .Values.things.dynamicConfig.filePath }}
473+
readOnly: true
474+
{{- end }}
462475
resources:
463476
requests:
464477
cpu: {{ mulf .Values.things.resources.cpu 1000 }}m
@@ -515,4 +528,9 @@ spec:
515528
{{- end }}
516529
- name: ditto-heap-dumps
517530
emptyDir: {}
531+
{{- if .Values.things.dynamicConfig.enabled }}
532+
- name: ditto-dynamic-config
533+
configMap:
534+
name: {{ .Release.Name }}-things-dynamic-config
535+
{{- end }}
518536
{{- end }}

deployment/helm/ditto/templates/thingssearch-deployment.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,14 @@ spec:
286286
value: "{{ .Values.thingsSearch.config.updater.backgroundSync.throttle.throughput }}"
287287
- name: BACKGROUND_SYCN_THROTTLE_PERIOD
288288
value: "{{ .Values.thingsSearch.config.updater.backgroundSync.throttle.period }}"
289+
{{- if .Values.thingsSearch.dynamicConfig.enabled }}
290+
- name: DITTO_DYNAMIC_CONFIG_ENABLED
291+
value: "true"
292+
- name: DITTO_DYNAMIC_CONFIG_FILE_PATH
293+
value: "{{ .Values.thingsSearch.dynamicConfig.filePath }}"
294+
- name: DITTO_DYNAMIC_CONFIG_POLL_INTERVAL
295+
value: "{{ .Values.thingsSearch.dynamicConfig.pollInterval }}"
296+
{{- end }}
289297
{{- if .Values.thingsSearch.extraEnv }}
290298
{{- toYaml .Values.thingsSearch.extraEnv | nindent 12 }}
291299
{{- end }}
@@ -359,6 +367,11 @@ spec:
359367
{{- end }}
360368
- name: ditto-heap-dumps
361369
mountPath: /opt/ditto/dumps
370+
{{- if .Values.thingsSearch.dynamicConfig.enabled }}
371+
- name: ditto-dynamic-config
372+
mountPath: {{ dir .Values.thingsSearch.dynamicConfig.filePath }}
373+
readOnly: true
374+
{{- end }}
362375
resources:
363376
requests:
364377
cpu: {{ mulf .Values.thingsSearch.resources.cpu 1000 }}m
@@ -415,4 +428,9 @@ spec:
415428
{{- end }}
416429
- name: ditto-heap-dumps
417430
emptyDir: {}
431+
{{- if .Values.thingsSearch.dynamicConfig.enabled }}
432+
- name: ditto-dynamic-config
433+
configMap:
434+
name: {{ .Release.Name }}-things-search-dynamic-config
435+
{{- end }}
418436
{{- end }}

deployment/helm/ditto/values.yaml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,24 @@ policies:
763763
enabled: false
764764
# interval: 30s
765765
# scrapeTimeout: 15s
766+
# dynamicConfig enables hot-reloading of HOCON configuration from a ConfigMap without pod restart.
767+
# When enabled, a ConfigMap is mounted at the specified file path and polled at the given interval.
768+
# Changes to the ConfigMap are detected and propagated to actors via Pekko EventStream.
769+
# content: |
770+
# ditto {
771+
# policies {
772+
# policy {
773+
# activity-check {
774+
# inactive-interval = 2h
775+
# }
776+
# }
777+
# }
778+
# }
779+
dynamicConfig:
780+
enabled: false
781+
pollInterval: 30s
782+
filePath: "/opt/ditto/dynamic-config/dynamic.conf"
783+
content: ""
766784
# config holds policies specific configuration
767785
config:
768786
# cluster contains policies specific clustering config
@@ -1102,6 +1120,22 @@ things:
11021120
enabled: false
11031121
# interval: 30s
11041122
# scrapeTimeout: 15s
1123+
# dynamicConfig enables hot-reloading of HOCON configuration from a ConfigMap without pod restart.
1124+
# content: |
1125+
# ditto {
1126+
# things {
1127+
# thing {
1128+
# activity-check {
1129+
# inactive-interval = 2h
1130+
# }
1131+
# }
1132+
# }
1133+
# }
1134+
dynamicConfig:
1135+
enabled: false
1136+
pollInterval: 30s
1137+
filePath: "/opt/ditto/dynamic-config/dynamic.conf"
1138+
content: ""
11051139
# config holds things specific configuration
11061140
config:
11071141
# cluster contains things specific clustering config
@@ -1601,6 +1635,22 @@ thingsSearch:
16011635
enabled: false
16021636
# interval: 30s
16031637
# scrapeTimeout: 15s
1638+
# dynamicConfig enables hot-reloading of HOCON configuration from a ConfigMap without pod restart.
1639+
# content: |
1640+
# ditto {
1641+
# search {
1642+
# operator-metrics {
1643+
# custom-metrics {
1644+
# my_metric { ... }
1645+
# }
1646+
# }
1647+
# }
1648+
# }
1649+
dynamicConfig:
1650+
enabled: false
1651+
pollInterval: 30s
1652+
filePath: "/opt/ditto/dynamic-config/dynamic.conf"
1653+
content: ""
16041654
# config holds things-search specific configuration
16051655
config:
16061656
# cluster contains things-search specific clustering config
@@ -1941,6 +1991,20 @@ connectivity:
19411991
enabled: false
19421992
# interval: 30s
19431993
# scrapeTimeout: 15s
1994+
# dynamicConfig enables hot-reloading of HOCON configuration from a ConfigMap without pod restart.
1995+
# content: |
1996+
# ditto {
1997+
# connectivity {
1998+
# connection {
1999+
# ...
2000+
# }
2001+
# }
2002+
# }
2003+
dynamicConfig:
2004+
enabled: false
2005+
pollInterval: 30s
2006+
filePath: "/opt/ditto/dynamic-config/dynamic.conf"
2007+
content: ""
19442008
# config holds connectivity specific configuration
19452009
config:
19462010
# cluster contains connectivity specific clustering config
@@ -2368,6 +2432,20 @@ gateway:
23682432
enabled: false
23692433
# interval: 30s
23702434
# scrapeTimeout: 15s
2435+
# dynamicConfig enables hot-reloading of HOCON configuration from a ConfigMap without pod restart.
2436+
# content: |
2437+
# ditto {
2438+
# gateway {
2439+
# streaming {
2440+
# ...
2441+
# }
2442+
# }
2443+
# }
2444+
dynamicConfig:
2445+
enabled: false
2446+
pollInterval: 30s
2447+
filePath: "/opt/ditto/dynamic-config/dynamic.conf"
2448+
content: ""
23712449
# config holds gateway specific configuration
23722450
config:
23732451
# cluster contains gateway specific clustering config

0 commit comments

Comments
 (0)