Repository: gardener/external-dns-management
Pull request #261 (2022-06-10 14:56:48)

Manage zone state caches globally
**What this PR does / why we need it**:
Currently, the zone state caches are managed per internal DNSAccount object.
A DNSAccount object is created per set of credentials as it is not possible to identify the account for all provider types.
If there are now multiple credentials for the same account, e.g. because of credentials rotation, there are multiple DNSAccount instances with separate zone state caches for the same hosted zone(s). 
The updates are always executed using the account of the provider of the DNSEntry. But all these updates are performed during a zone reconciliation by comparing to a single cached zone state from one account. This can lead to phantom or missing records in this particular cache and update operations are added to "repair" the mismatches. These update operations will fail und the updates are added on the next reconciliation again until the zone state caches is synchronised periodically.

Using the newly introduced qualified zone ids, the zone state caches are now managed globally. This means there is only one zone state cache per hosted zone globally, independent of the number of DNSAccounts.

Addtionally the option to store the caches in a persistent volume has been removed, as it was never used and adjusting it to the new structure was too much effort.

**Which issue(s) this PR fixes**:
Fixes #

**Special notes for your reviewer**:

**Release note**:
<!--  Write your release note:
1. Enter your release note in the below block.
2. If no release note is required, just write "NONE" within the block.

Format of block header: <category> <target_group>
Possible values:
- category:       breaking|feature|bugfix|doc|other
- target_group:   user|operator|developer|dependency
-->
```bugfix operator
Manage zone state cache globally
```

```breaking operator
Persisting zone state caches with option `--cache-dir` is not supported anymore.
```



Patch:
diff --git a/README.md b/README.md
index f5e6fef6..a4a8fef5 100644
--- a/README.md
+++ b/README.md
@@ -431,7 +431,6 @@ Flags:
       --azure-private-dns.ratelimiter.qps int                         maximum requests/queries per second
       --bind-address-http string                                      HTTP server bind address
       --blocked-zone zone-id                                          Blocks a zone given in the format zone-id from a provider as if the zone is not existing.
-      --cache-dir string                                              Directory to store zone caches (for reload after restart)
       --cache-ttl int                                                 Time-to-live for provider hosted zone cache
       --cloudflare-dns.advanced.batch-size int                        batch size for change requests (currently only used for aws-route53)
       --cloudflare-dns.advanced.max-retries int                       maximum number of retries to avoid paging stops on throttling (currently only used for aws-route53)
@@ -466,7 +465,6 @@ Flags:
       --compound.azure-private-dns.ratelimiter.enabled                enables rate limiter for DNS provider requests of controller compound
       --compound.azure-private-dns.ratelimiter.qps int                maximum requests/queries per second of controller compound
       --compound.blocked-zone zone-id                                 Blocks a zone given in the format zone-id from a provider as if the zone is not existing. of controller compound
-      --compound.cache-dir string                                     Directory to store zone caches (for reload after restart) of controller compound
       --compound.cache-ttl int                                        Time-to-live for provider hosted zone cache of controller compound
       --compound.cloudflare-dns.advanced.batch-size int               batch size for change requests (currently only used for aws-route53) of controller compound
       --compound.cloudflare-dns.advanced.max-retries int              maximum number of retries to avoid paging stops on throttling (currently only used for aws-route53) of controller compound
diff --git a/charts/external-dns-management/templates/deployment.yaml b/charts/external-dns-management/templates/deployment.yaml
index 43208670..26f393e9 100644
--- a/charts/external-dns-management/templates/deployment.yaml
+++ b/charts/external-dns-management/templates/deployment.yaml
@@ -58,9 +58,6 @@ spec:
         - --identifier=seed.gardener.cloud/{{ .Values.gardener.garden.clusterIdentity }}/{{ .Values.gardener.seed.identity }}
         - --remote-access-client-id={{ .Values.gardener.seed.identity }}
         {{- end }}
-        {{- if .Values.configuration.persistentCache }}
-        - --cache-dir=/persistent-cache
-        {{- end }}
         {{- if .Values.remoteaccess.enabled }}
         - --remote-access-port={{ .Values.remoteaccess.port }}
         - --remote-access-cacert=/certs/ca/ca.crt
@@ -755,6 +752,12 @@ spec:
         {{- if .Values.configuration.remoteRatelimiterQps }}
         - --remote.ratelimiter.qps={{ .Values.configuration.remoteRatelimiterQps }}
         {{- end }}
+        {{- if .Values.configuration.remoteaccesscertificatesDefaultPoolSize }}
+        - --remoteaccesscertificates.default.pool.size={{ .Values.configuration.remoteaccesscertificatesDefaultPoolSize }}
+        {{- end }}
+        {{- if .Values.configuration.remoteaccesscertificatesPoolSize }}
+        - --remoteaccesscertificates.pool.size={{ .Values.configuration.remoteaccesscertificatesPoolSize }}
+        {{- end }}
         {{- if .Values.configuration.rescheduleDelay }}
         - --reschedule-delay={{ .Values.configuration.rescheduleDelay }}
         {{- end }}
@@ -891,12 +894,8 @@ spec:
         {{- end }}
         resources:
           {{- toYaml .Values.resources | nindent 10 }}
-        {{- if or .Values.configuration.persistentCache .Values.custom.volumeMounts .Values.remoteaccess.enabled }}
+        {{- if or .Values.custom.volumeMounts .Values.remoteaccess.enabled }}
         volumeMounts:
-          {{- if or .Values.configuration.persistentCache }}
-          - name: persistent-cache
-            mountPath: /persistent-cache
-          {{- end }}
           {{- if .Values.custom.volumeMounts }}
           {{- toYaml .Values.custom.volumeMounts | nindent 10 }}
           {{- end }}
@@ -905,13 +904,8 @@ spec:
             name: certs-ca
           {{- end }}
         {{- end }}
-      {{- if or .Values.configuration.persistentCache .Values.custom.volumes .Values.remoteaccess.enabled }}
+      {{- if or .Values.custom.volumes .Values.remoteaccess.enabled }}
       volumes:
-        {{- if or .Values.configuration.persistentCache }}
-        - name: persistent-cache
-          persistentVolumeClaim:
-            claimName: {{ include "external-dns-management.fullname" . }}
-        {{- end }}
         {{- if .Values.remoteaccess.enabled }}
         - name: certs-ca
           secret:
diff --git a/charts/external-dns-management/templates/psp.yaml b/charts/external-dns-management/templates/psp.yaml
index e4ff7d04..00e0c539 100644
--- a/charts/external-dns-management/templates/psp.yaml
+++ b/charts/external-dns-management/templates/psp.yaml
@@ -27,9 +27,6 @@ spec:
   - ALL
   volumes:
   - secret
-{{- if .Values.configuration.persistentCache }}
-  - persistentVolumeClaim
-{{- end }}
   hostNetwork: false
   hostIPC: false
   hostPID: false
diff --git a/charts/external-dns-management/templates/pvc.yaml b/charts/external-dns-management/templates/pvc.yaml
deleted file mode 100644
index da65455e..00000000
--- a/charts/external-dns-management/templates/pvc.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
----
-{{- if .Values.configuration.persistentCache }}
-apiVersion: v1
-kind: PersistentVolumeClaim
-metadata:
-  name: {{ include "external-dns-management.fullname" . }}
-  namespace: {{ .Release.Namespace }}
-  labels:
-    app: {{ include "external-dns-management.fullname" . }}
-spec:
-  accessModes:
-    - ReadWriteOnce
-  resources:
-    requests:
-      {{- if not (eq .Values.gardener.seed.provider "alicloud") }}
-      storage: {{ .Values.configuration.persistentCacheStorageSize }}
-      {{- else }}
-      storage: {{ .Values.configuration.persistentCacheStorageSizeAlicloud }}
-      {{- end }}
-{{- end }}
diff --git a/charts/external-dns-management/values.yaml b/charts/external-dns-management/values.yaml
index 0b58d7b7..55d20511 100644
--- a/charts/external-dns-management/values.yaml
+++ b/charts/external-dns-management/values.yaml
@@ -278,6 +278,8 @@ configuration:
   # remoteRatelimiterBurst:
   # remoteRatelimiterEnabled:
   # remoteRatelimiterQps:
+  # remoteaccesscertificatesDefaultPoolSize:
+  # remoteaccesscertificatesPoolSize:
   # rescheduleDelay: 120s
   # secretsPoolSize:
   serverPortHttp: 8080
diff --git a/hack/generateChartOptions.py b/hack/generateChartOptions.py
index 106bc619..84eb0409 100644
--- a/hack/generateChartOptions.py
+++ b/hack/generateChartOptions.py
@@ -52,9 +52,6 @@ for line in options.split("\n"):
 
 defaultValues = {
   "controllers": "all",
-  "persistentCache": "false",
-  "persistentCacheStorageSize": "1Gi",
-  "persistentCacheStorageSizeAlicloud": "20Gi",
   "serverPortHttp": "8080",
   "ttl": 120,
 }
diff --git a/pkg/controller/provider/alicloud/handler.go b/pkg/controller/provider/alicloud/handler.go
index 255960b3..193d2056 100644
--- a/pkg/controller/provider/alicloud/handler.go
+++ b/pkg/controller/provider/alicloud/handler.go
@@ -59,7 +59,7 @@ func NewHandler(c *provider.DNSHandlerConfig) (provider.DNSHandler, error) {
 
 	h.access = access
 
-	h.cache, err = provider.NewZoneCache(*c.CacheConfig.CopyWithDisabledZoneStateCache(), c.Metrics, nil, h.getZones, h.getZoneState)
+	h.cache, err = c.ZoneCacheFactory.CreateZoneCache(provider.CacheZonesOnly, c.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/controller/provider/aws/handler.go b/pkg/controller/provider/aws/handler.go
index 4eae6771..a48d65fb 100644
--- a/pkg/controller/provider/aws/handler.go
+++ b/pkg/controller/provider/aws/handler.go
@@ -113,8 +113,7 @@ func NewHandler(c *provider.DNSHandlerConfig) (provider.DNSHandler, error) {
 	h.sess = sess
 	h.r53 = route53.New(sess)
 
-	forwardedDomains := provider.NewForwardedDomainsHandlerData()
-	h.cache, err = provider.NewZoneCache(c.CacheConfig, c.Metrics, forwardedDomains, h.getZones, h.getZoneState)
+	h.cache, err = c.ZoneCacheFactory.CreateZoneCache(provider.CacheZoneState, c.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
@@ -170,7 +169,7 @@ func (h *Handler) getZones(cache provider.ZoneCache) (provider.DNSHostedZones, e
 		// call GetZoneState for side effect to calculate forwarded domains
 		_, err := cache.GetZoneState(hostedZone)
 		if err == nil {
-			forwarded := cache.GetHandlerData().(*provider.ForwardedDomainsHandlerData).GetForwardedDomains(hostedZone.Id())
+			forwarded := cache.ForwardedDomainsCache().Get(hostedZone.Id())
 			if forwarded != nil {
 				hostedZone = provider.CopyDNSHostedZone(hostedZone, forwarded)
 			}
@@ -227,7 +226,7 @@ func (h *Handler) getZoneState(zone provider.DNSHostedZone, cache provider.ZoneC
 		return nil, err
 	}
 
-	cache.GetHandlerData().(*provider.ForwardedDomainsHandlerData).SetForwardedDomains(zone.Id(), forwarded)
+	cache.ForwardedDomainsCache().Set(zone.Id(), forwarded)
 
 	return provider.NewDNSZoneState(dnssets), nil
 }
diff --git a/pkg/controller/provider/azure-private/handler.go b/pkg/controller/provider/azure-private/handler.go
index 79e9c8f0..53ddd43c 100644
--- a/pkg/controller/provider/azure-private/handler.go
+++ b/pkg/controller/provider/azure-private/handler.go
@@ -73,7 +73,7 @@ func NewHandler(c *provider.DNSHandlerConfig) (provider.DNSHandler, error) {
 	h.zonesClient = &zonesClient
 	h.recordsClient = &recordsClient
 
-	h.cache, err = provider.NewZoneCache(c.CacheConfig, c.Metrics, nil, h.getZones, h.getZoneState)
+	h.cache, err = c.ZoneCacheFactory.CreateZoneCache(provider.CacheZoneState, c.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/controller/provider/azure/handler.go b/pkg/controller/provider/azure/handler.go
index afc8cdce..edf3ebb9 100644
--- a/pkg/controller/provider/azure/handler.go
+++ b/pkg/controller/provider/azure/handler.go
@@ -73,7 +73,7 @@ func NewHandler(c *provider.DNSHandlerConfig) (provider.DNSHandler, error) {
 	h.zonesClient = &zonesClient
 	h.recordsClient = &recordsClient
 
-	h.cache, err = provider.NewZoneCache(c.CacheConfig, c.Metrics, nil, h.getZones, h.getZoneState)
+	h.cache, err = c.ZoneCacheFactory.CreateZoneCache(provider.CacheZoneState, c.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/controller/provider/cloudflare/handler.go b/pkg/controller/provider/cloudflare/handler.go
index 80f60d0b..c97e88fa 100644
--- a/pkg/controller/provider/cloudflare/handler.go
+++ b/pkg/controller/provider/cloudflare/handler.go
@@ -60,7 +60,7 @@ func NewHandler(c *provider.DNSHandlerConfig) (provider.DNSHandler, error) {
 
 	h.access = access
 
-	h.cache, err = provider.NewZoneCache(*c.CacheConfig.CopyWithDisabledZoneStateCache(), c.Metrics, nil, h.getZones, h.getZoneState)
+	h.cache, err = c.ZoneCacheFactory.CreateZoneCache(provider.CacheZonesOnly, c.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/controller/provider/google/handler.go b/pkg/controller/provider/google/handler.go
index 59a073c3..c3c600b6 100644
--- a/pkg/controller/provider/google/handler.go
+++ b/pkg/controller/provider/google/handler.go
@@ -86,8 +86,7 @@ func NewHandler(config *provider.DNSHandlerConfig) (provider.DNSHandler, error)
 		return nil, err
 	}
 
-	forwardedDomains := provider.NewForwardedDomainsHandlerData()
-	h.cache, err = provider.NewZoneCache(config.CacheConfig, config.Metrics, forwardedDomains, h.getZones, h.getZoneState)
+	h.cache, err = config.ZoneCacheFactory.CreateZoneCache(provider.CacheZoneState, config.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
@@ -135,7 +134,7 @@ func (h *Handler) getZones(cache provider.ZoneCache) (provider.DNSHostedZones, e
 		// call GetZoneState for side effect to calculate forwarded domains
 		_, err := cache.GetZoneState(hostedZone)
 		if err == nil {
-			forwarded := cache.GetHandlerData().(*provider.ForwardedDomainsHandlerData).GetForwardedDomains(hostedZone.Id())
+			forwarded := cache.ForwardedDomainsCache().Get(hostedZone.Id())
 			if forwarded != nil {
 				hostedZone = provider.CopyDNSHostedZone(hostedZone, forwarded)
 			}
@@ -191,7 +190,7 @@ func (h *Handler) getZoneState(zone provider.DNSHostedZone, cache provider.ZoneC
 	if err != nil {
 		return nil, err
 	}
-	cache.GetHandlerData().(*provider.ForwardedDomainsHandlerData).SetForwardedDomains(zone.Id(), forwarded)
+	cache.ForwardedDomainsCache().Set(zone.Id(), forwarded)
 
 	return provider.NewDNSZoneState(dnssets), nil
 }
diff --git a/pkg/controller/provider/infoblox/handler.go b/pkg/controller/provider/infoblox/handler.go
index 15ae0927..d83508f7 100644
--- a/pkg/controller/provider/infoblox/handler.go
+++ b/pkg/controller/provider/infoblox/handler.go
@@ -166,7 +166,7 @@ func NewHandler(config *provider.DNSHandlerConfig) (provider.DNSHandler, error)
 
 	h.access = NewAccess(client, *h.infobloxConfig.View, config.Metrics)
 
-	h.ZoneCache, err = provider.NewZoneCache(*config.CacheConfig.CopyWithDisabledZoneStateCache(), config.Metrics, nil, h.getZones, h.getZoneState)
+	h.ZoneCache, err = config.ZoneCacheFactory.CreateZoneCache(provider.CacheZonesOnly, config.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/controller/provider/mock/handler.go b/pkg/controller/provider/mock/handler.go
index d57fce95..a720d797 100644
--- a/pkg/controller/provider/mock/handler.go
+++ b/pkg/controller/provider/mock/handler.go
@@ -85,7 +85,7 @@ func NewHandler(config *provider.DNSHandlerConfig) (provider.DNSHandler, error)
 		}
 	}
 
-	h.cache, err = provider.NewZoneCache(config.CacheConfig, config.Metrics, nil, h.getZones, h.getZoneState)
+	h.cache, err = config.ZoneCacheFactory.CreateZoneCache(provider.CacheZoneState, config.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/controller/provider/netlify/handler.go b/pkg/controller/provider/netlify/handler.go
index c7076007..f0db1318 100644
--- a/pkg/controller/provider/netlify/handler.go
+++ b/pkg/controller/provider/netlify/handler.go
@@ -56,7 +56,7 @@ func NewHandler(c *provider.DNSHandlerConfig) (provider.DNSHandler, error) {
 
 	h.access = access
 
-	h.cache, err = provider.NewZoneCache(*c.CacheConfig.CopyWithDisabledZoneStateCache(), c.Metrics, nil, h.getZones, h.getZoneState)
+	h.cache, err = c.ZoneCacheFactory.CreateZoneCache(provider.CacheZonesOnly, c.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/controller/provider/openstack/handler.go b/pkg/controller/provider/openstack/handler.go
index c4e78393..f6e662c4 100644
--- a/pkg/controller/provider/openstack/handler.go
+++ b/pkg/controller/provider/openstack/handler.go
@@ -61,7 +61,7 @@ func NewHandler(config *provider.DNSHandlerConfig) (provider.DNSHandler, error)
 		client:            designateClient{serviceClient: serviceClient, metrics: config.Metrics},
 	}
 
-	h.cache, err = provider.NewZoneCache(config.CacheConfig, config.Metrics, nil, h.getZones, h.getZoneState)
+	h.cache, err = config.ZoneCacheFactory.CreateZoneCache(provider.CacheZoneState, config.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/controller/provider/remote/handler.go b/pkg/controller/provider/remote/handler.go
index 2302255c..1371f550 100644
--- a/pkg/controller/provider/remote/handler.go
+++ b/pkg/controller/provider/remote/handler.go
@@ -101,7 +101,7 @@ func NewHandler(c *provider.DNSHandlerConfig) (provider.DNSHandler, error) {
 	}
 	h.client = common.NewRemoteProviderClient(h.connection)
 
-	h.cache, err = provider.NewZoneCache(c.CacheConfig, c.Metrics, nil, h.getZones, h.getZoneState)
+	h.cache, err = c.ZoneCacheFactory.CreateZoneCache(provider.CacheZoneState, c.Metrics, h.getZones, h.getZoneState)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/dns/provider/const.go b/pkg/dns/provider/const.go
index bed5050b..6e7a7fb5 100644
--- a/pkg/dns/provider/const.go
+++ b/pkg/dns/provider/const.go
@@ -31,7 +31,6 @@ const (
 	OPT_DRYRUN                     = "dry-run"
 	OPT_TTL                        = "ttl"
 	OPT_CACHE_TTL                  = "cache-ttl"
-	OPT_CACHE_DIR                  = "cache-dir"
 	OPT_SETUP                      = dns.OPT_SETUP
 	OPT_DNSDELAY                   = "dns-delay"
 	OPT_RESCHEDULEDELAY            = "reschedule-delay"
diff --git a/pkg/dns/provider/controller.go b/pkg/dns/provider/controller.go
index be8c416a..698590fd 100644
--- a/pkg/dns/provider/controller.go
+++ b/pkg/dns/provider/controller.go
@@ -120,7 +120,6 @@ func DNSController(name string, factory DNSHandlerFactory) controller.Configurat
 		RequireLease().
 		DefaultedStringOption(OPT_CLASS, dns.DEFAULT_CLASS, "Class identifier used to differentiate responsible controllers for entry resources").
 		DefaultedStringOption(OPT_IDENTIFIER, "dnscontroller", "Identifier used to mark DNS entries in DNS system").
-		DefaultedStringOption(OPT_CACHE_DIR, "", "Directory to store zone caches (for reload after restart)").
 		DefaultedBoolOption(OPT_DRYRUN, false, "just check, don't modify").
 		DefaultedBoolOption(OPT_DISABLE_ZONE_STATE_CACHING, false, "disable use of cached dns zone state on changes").
 		DefaultedIntOption(OPT_TTL, 300, "Default time-to-live for DNS entries. Defines how long the record is kept in cache by DNS servers or resolvers.").
@@ -200,8 +199,6 @@ func Create(c controller.Interface, factory DNSHandlerFactory) (reconcile.Interf
 		return nil, err
 	}
 
-	zoneCacheCleanupOutdated(c, config.CacheDir, ZoneCachePrefix)
-
 	ownerresc, err := c.GetCluster(TARGET_CLUSTER).Resources().GetByGK(ownerGroupKind)
 	if err != nil {
 		return nil, err
diff --git a/pkg/dns/provider/inmemory.go b/pkg/dns/provider/inmemory.go
index d80b2fe6..a216b418 100644
--- a/pkg/dns/provider/inmemory.go
+++ b/pkg/dns/provider/inmemory.go
@@ -80,10 +80,10 @@ func (m *InMemory) SetZone(zone DNSHostedZone, zoneState DNSZoneState) {
 	m.zones[zone.Id()] = zonedata{zone: zone, dnssets: clone}
 }
 
-func (m *InMemory) DeleteZone(zone DNSHostedZone) {
+func (m *InMemory) DeleteZone(zoneID dns.ZoneID) {
 	m.lock.Lock()
 	defer m.lock.Unlock()
-	delete(m.zones, zone.Id())
+	delete(m.zones, zoneID)
 }
 
 func (m *InMemory) AddZone(zone DNSHostedZone) bool {
diff --git a/pkg/dns/provider/interface.go b/pkg/dns/provider/interface.go
index bf825357..ab5f6748 100644
--- a/pkg/dns/provider/interface.go
+++ b/pkg/dns/provider/interface.go
@@ -37,7 +37,6 @@ import (
 type Config struct {
 	TTL                int64
 	CacheTTL           time.Duration
-	CacheDir           string
 	RescheduleDelay    time.Duration
 	StatusCheckPeriod  time.Duration
 	Ident              string
@@ -63,7 +62,6 @@ func NewConfigForController(c controller.Interface, factory DNSHandlerFactory) (
 	if err != nil {
 		cttl = 60
 	}
-	cdir, _ := c.GetStringOption(OPT_CACHE_DIR)
 	dryrun, _ := c.GetBoolOption(OPT_DRYRUN)
 
 	delay, err := c.GetDurationOption(OPT_DNSDELAY)
@@ -117,7 +115,6 @@ func NewConfigForController(c controller.Interface, factory DNSHandlerFactory) (
 		Ident:              ident,
 		TTL:                int64(ttl),
 		CacheTTL:           time.Duration(cttl) * time.Second,
-		CacheDir:           cdir,
 		RescheduleDelay:    rescheduleDelay,
 		StatusCheckPeriod:  statuscheckperiod,
 		Dryrun:             dryrun,
@@ -158,15 +155,15 @@ outer:
 }
 
 type DNSHandlerConfig struct {
-	Logger      logger.LogContext
-	Properties  utils.Properties
-	Config      *runtime.RawExtension
-	DryRun      bool
-	Context     context.Context
-	CacheConfig ZoneCacheConfig
-	Options     *FactoryOptions
-	Metrics     Metrics
-	RateLimiter flowcontrol.RateLimiter
+	Logger           logger.LogContext
+	Properties       utils.Properties
+	Config           *runtime.RawExtension
+	DryRun           bool
+	Context          context.Context
+	ZoneCacheFactory ZoneCacheFactory
+	Options          *FactoryOptions
+	Metrics          Metrics
+	RateLimiter      flowcontrol.RateLimiter
 }
 
 type DNSZoneState interface {
diff --git a/pkg/dns/provider/provider.go b/pkg/dns/provider/provider.go
index 129689bf..6c27e675 100644
--- a/pkg/dns/provider/provider.go
+++ b/pkg/dns/provider/provider.go
@@ -20,7 +20,6 @@ import (
 	"crypto/sha256"
 	"encoding/hex"
 	"fmt"
-	"path/filepath"
 	"reflect"
 	"sort"
 	"strings"
@@ -139,15 +138,13 @@ func (this *DNSAccount) Release() {
 type AccountCache struct {
 	lock    sync.Mutex
 	ttl     time.Duration
-	dir     string
 	cache   map[string]*DNSAccount
 	options *FactoryOptions
 }
 
-func NewAccountCache(ttl time.Duration, dir string, opts *FactoryOptions) *AccountCache {
+func NewAccountCache(ttl time.Duration, opts *FactoryOptions) *AccountCache {
 	return &AccountCache{
 		ttl:   ttl,
-		dir:   dir,
 		cache: map[string]*DNSAccount{},
 
 		options: opts,
@@ -166,28 +163,23 @@ func (this *AccountCache) Get(logger logger.LogContext, provider *dnsutils.DNSPr
 		if syncPeriod == nil {
 			return nil, fmt.Errorf("Pool dns not found")
 		}
-		persistDir := ""
-		if this.dir != "" {
-			persistDir = filepath.Join(this.dir, ZoneCachePrefix+hash)
-		}
-		cacheConfig := ZoneCacheConfig{
+		cacheFactory := ZoneCacheFactory{
 			context:               state.GetContext().GetContext(),
 			logger:                logger,
-			persistDir:            persistDir,
 			zonesTTL:              this.ttl,
-			stateTTLGetter:        state.CreateStateTTLGetter(*syncPeriod),
+			zoneStates:            state.zoneStates,
 			disableZoneStateCache: !state.config.ZoneStateCaching,
 		}
 
 		cfg := DNSHandlerConfig{
-			Context:     state.GetContext().GetContext(),
-			Logger:      logger,
-			Properties:  props,
-			Config:      provider.Spec().ProviderConfig,
-			DryRun:      state.GetConfig().Dryrun,
-			CacheConfig: cacheConfig,
-			Options:     this.options,
-			Metrics:     a,
+			Context:          state.GetContext().GetContext(),
+			Logger:           logger,
+			Properties:       props,
+			Config:           provider.Spec().ProviderConfig,
+			DryRun:           state.GetConfig().Dryrun,
+			ZoneCacheFactory: cacheFactory,
+			Options:          this.options,
+			Metrics:          a,
 		}
 		var err error
 		a.handler, err = state.GetHandlerFactory().Create(provider.TypeCode(), &cfg)
diff --git a/pkg/dns/provider/state.go b/pkg/dns/provider/state.go
index 897e8d70..af41fc35 100644
--- a/pkg/dns/provider/state.go
+++ b/pkg/dns/provider/state.go
@@ -128,6 +128,7 @@ type state struct {
 
 	accountCache *AccountCache
 	ownerCache   *OwnerCache
+	zoneStates   *zoneStates
 
 	foreign         map[resources.ObjectName]*foreignProvider
 	providers       map[resources.ObjectName]*dnsProviderVersion
@@ -172,7 +173,6 @@ func NewDNSState(ctx Context, ownerresc, secretresc resources.Interface, classes
 	ctx.Infof("dry run mode:                %t", config.Dryrun)
 	ctx.Infof("reschedule delay:            %v", config.RescheduleDelay)
 	ctx.Infof("zone cache ttl for zones:    %v", config.CacheTTL)
-	ctx.Infof("zone cache persist dir:      %s", config.CacheDir)
 	ctx.Infof("disable zone state caching:  %t", !config.ZoneStateCaching)
 	if config.RemoteAccessConfig != nil {
 		ctx.Infof("remote access server port: %d", config.RemoteAccessConfig.Port)
@@ -188,7 +188,7 @@ func NewDNSState(ctx Context, ownerresc, secretresc resources.Interface, classes
 		secretresc:          secretresc,
 		config:              config,
 		realms:              realms,
-		accountCache:        NewAccountCache(config.CacheTTL, config.CacheDir, config.Options),
+		accountCache:        NewAccountCache(config.CacheTTL, config.Options),
 		ownerCache:          NewOwnerCache(ctx, &config),
 		foreign:             map[resources.ObjectName]*foreignProvider{},
 		providers:           map[resources.ObjectName]*dnsProviderVersion{},
@@ -213,6 +213,11 @@ func (this *state) IsResponsibleFor(logger logger.LogContext, obj resources.Obje
 }
 
 func (this *state) Setup() error {
+	syncPeriod := this.context.GetPoolPeriod(DNS_POOL)
+	if syncPeriod == nil {
+		return fmt.Errorf("Pool %s not found", DNS_POOL)
+	}
+	this.zoneStates = newZoneStates(this.CreateStateTTLGetter(*syncPeriod))
 	this.dnsTicker = NewTicker(this.context.GetPool(DNS_POOL).Tick)
 	this.ownerupd = startOwnerUpdater(this.context, this.ownerresc)
 	processors, err := this.context.GetIntOption(OPT_SETUP)
diff --git a/pkg/dns/provider/zonecache.go b/pkg/dns/provider/zonecache.go
index 42c2230e..7f07f4b8 100644
--- a/pkg/dns/provider/zonecache.go
+++ b/pkg/dns/provider/zonecache.go
@@ -18,19 +18,15 @@ package provider
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
+	"reflect"
+	"sort"
 	"strings"
 	"sync"
-	"sync/atomic"
 	"time"
 
 	"github.com/gardener/controller-manager-library/pkg/logger"
 	"github.com/gardener/external-dns-management/pkg/server/metrics"
-	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/gardener/external-dns-management/pkg/dns"
 	"github.com/gardener/external-dns-management/pkg/dns/provider/errors"
@@ -38,28 +34,46 @@ import (
 
 type StateTTLGetter func(zoneid dns.ZoneID) time.Duration
 
-type ZoneCacheConfig struct {
+type ZoneCacheFactory struct {
 	context               context.Context
 	logger                logger.LogContext
-	zoneID                string
-	providerType          string
-	persistDir            string
 	zonesTTL              time.Duration
-	stateTTLGetter        StateTTLGetter
+	zoneStates            *zoneStates
 	disableZoneStateCache bool
 }
 
-func NewTestZoneCacheConfig(zonesTTL, stateTTL time.Duration) *ZoneCacheConfig {
-	return &ZoneCacheConfig{
-		zonesTTL:       zonesTTL,
-		stateTTLGetter: func(id dns.ZoneID) time.Duration { return stateTTL },
+func (c ZoneCacheFactory) CreateZoneCache(cacheType ZoneCacheType, metrics Metrics, zonesUpdater ZoneCacheZoneUpdater, stateUpdater ZoneCacheStateUpdater) (ZoneCache, error) {
+	common := abstractZonesCache{zonesTTL: c.zonesTTL, logger: c.logger, zonesUpdater: zonesUpdater, stateUpdater: stateUpdater}
+	switch cacheType {
+	case CacheZonesOnly:
+		cache := &onlyZonesCache{abstractZonesCache: common}
+		return cache, nil
+	case CacheZoneState:
+		if c.disableZoneStateCache {
+			cache := &onlyZonesCache{abstractZonesCache: common}
+			return cache, nil
+		}
+		return newDefaultZoneCache(c.zoneStates, common, metrics)
+	default:
+		return nil, fmt.Errorf("unknown zone cache type: %v", cacheType)
 	}
 }
 
-func (c *ZoneCacheConfig) CopyWithDisabledZoneStateCache() *ZoneCacheConfig {
-	return &ZoneCacheConfig{context: c.context, logger: c.logger,
-		zoneID: c.zoneID, providerType: c.providerType,
-		persistDir: c.persistDir, zonesTTL: c.zonesTTL, stateTTLGetter: c.stateTTLGetter, disableZoneStateCache: true}
+// ZoneCacheType is the zone cache type.
+type ZoneCacheType int
+
+const (
+	// CacheZonesOnly only caches the zones of the account, but not the zone state itself.
+	CacheZonesOnly ZoneCacheType = iota
+	// CacheZoneState caches both zones of the account and the zone states as needed.
+	CacheZoneState
+)
+
+func NewTestZoneCacheFactory(zonesTTL, stateTTL time.Duration) *ZoneCacheFactory {
+	return &ZoneCacheFactory{
+		zonesTTL:   zonesTTL,
+		zoneStates: newZoneStates(func(id dns.ZoneID) time.Duration { return stateTTL }),
+	}
 }
 
 type ZoneCacheZoneUpdater func(cache ZoneCache) (DNSHostedZones, error)
@@ -70,46 +84,32 @@ type ZoneCache interface {
 	GetZones() (DNSHostedZones, error)
 	GetZoneState(zone DNSHostedZone) (DNSZoneState, error)
 	ApplyRequests(logctx logger.LogContext, err error, zone DNSHostedZone, reqs []*ChangeRequest)
-	GetHandlerData() HandlerData
+	ForwardedDomainsCache() ForwardedDomainsCache
 	Release()
 	ReportZoneStateConflict(zone DNSHostedZone, err error) bool
 }
 
-type HandlerData interface {
-	Marshal(zoneID dns.ZoneID) (*PersistentHandlerData, error)
-	Unmarshal(zoneID dns.ZoneID, data *PersistentHandlerData) error
-	DeleteZone(zoneID dns.ZoneID)
-}
-
-func NewZoneCache(config ZoneCacheConfig, metrics Metrics, handlerData HandlerData,
-	zonesUpdater ZoneCacheZoneUpdater, stateUpdater ZoneCacheStateUpdater) (ZoneCache, error) {
-	common := abstractZonesCache{config: config, zonesUpdater: zonesUpdater, stateUpdater: stateUpdater}
-	if config.disableZoneStateCache {
-		cache := &onlyZonesCache{abstractZonesCache: common, handlerData: handlerData}
-		return cache, nil
-	} else {
-		return newDefaultZoneCache(common, metrics, handlerData)
-	}
+type ForwardedDomainsCache interface {
+	Get(zoneid dns.ZoneID) []string
+	Set(zoneid dns.ZoneID, value []string)
 }
 
-type ForwardedDomainsHandlerData struct {
+type forwardedDomainsCacheImpl struct {
 	lock             sync.Mutex
 	forwardedDomains map[dns.ZoneID][]string
 }
 
-func NewForwardedDomainsHandlerData() *ForwardedDomainsHandlerData {
-	return &ForwardedDomainsHandlerData{forwardedDomains: map[dns.ZoneID][]string{}}
+func newForwardedDomainsCacheImpl() *forwardedDomainsCacheImpl {
+	return &forwardedDomainsCacheImpl{forwardedDomains: map[dns.ZoneID][]string{}}
 }
 
-var _ HandlerData = &ForwardedDomainsHandlerData{}
-
-func (hd *ForwardedDomainsHandlerData) GetForwardedDomains(zoneid dns.ZoneID) []string {
+func (hd *forwardedDomainsCacheImpl) Get(zoneid dns.ZoneID) []string {
 	hd.lock.Lock()
 	defer hd.lock.Unlock()
 	return hd.forwardedDomains[zoneid]
 }
 
-func (hd *ForwardedDomainsHandlerData) SetForwardedDomains(zoneid dns.ZoneID, value []string) {
+func (hd *forwardedDomainsCacheImpl) Set(zoneid dns.ZoneID, value []string) {
 	hd.lock.Lock()
 	defer hd.lock.Unlock()
 
@@ -120,45 +120,7 @@ func (hd *ForwardedDomainsHandlerData) SetForwardedDomains(zoneid dns.ZoneID, va
 	}
 }
 
-func (hd *ForwardedDomainsHandlerData) Marshal(zoneID dns.ZoneID) (*PersistentHandlerData, error) {
-	hd.lock.Lock()
-	defer hd.lock.Unlock()
-
-	data := hd.forwardedDomains[zoneID]
-	if data == nil {
-		return nil, nil
-	}
-	bytes, err := json.Marshal(data)
-	if err != nil {
-		return nil, err
-	}
-	return &PersistentHandlerData{Name: "ForwardedDomains", Version: "1", Value: &runtime.RawExtension{Raw: bytes}}, nil
-}
-
-func (hd *ForwardedDomainsHandlerData) Unmarshal(zoneID dns.ZoneID, data *PersistentHandlerData) error {
-	hd.lock.Lock()
-	defer hd.lock.Unlock()
-
-	if data == nil {
-		return nil
-	}
-	if data.Name != "ForwardedDomains" || data.Version != "1" {
-		return fmt.Errorf("unexpected HandlerData: %s %s", data.Name, data.Version)
-	}
-	if data.Value == nil {
-		return fmt.Errorf("missing value in HandlerData: %s %s", data.Name, data.Version)
-	}
-
-	var value []string
-	err := json.Unmarshal([]byte(data.Value.Raw), &value)
-	if err != nil {
-		return err
-	}
-	hd.forwardedDomains[zoneID] = value
-	return nil
-}
-
-func (hd *ForwardedDomainsHandlerData) DeleteZone(zoneID dns.ZoneID) {
+func (hd *forwardedDomainsCacheImpl) DeleteZone(zoneID dns.ZoneID) {
 	hd.lock.Lock()
 	defer hd.lock.Unlock()
 
@@ -166,7 +128,8 @@ func (hd *ForwardedDomainsHandlerData) DeleteZone(zoneID dns.ZoneID) {
 }
 
 type abstractZonesCache struct {
-	config       ZoneCacheConfig
+	logger       logger.LogContext
+	zonesTTL     time.Duration
 	zones        DNSHostedZones
 	zonesErr     error
 	zonesNext    time.Time
@@ -176,8 +139,8 @@ type abstractZonesCache struct {
 
 type onlyZonesCache struct {
 	abstractZonesCache
-	lock        sync.Mutex
-	handlerData HandlerData
+	lock                  sync.Mutex
+	forwardedDomainsCache ForwardedDomainsCache
 }
 
 var _ ZoneCache = &onlyZonesCache{}
@@ -195,8 +158,13 @@ func (c *onlyZonesCache) GetZoneState(zone DNSHostedZone) (DNSZoneState, error)
 func (c *onlyZonesCache) ApplyRequests(logctx logger.LogContext, err error, zone DNSHostedZone, reqs []*ChangeRequest) {
 }
 
-func (c *onlyZonesCache) GetHandlerData() HandlerData {
-	return c.handlerData
+func (c *onlyZonesCache) ForwardedDomainsCache() ForwardedDomainsCache {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	if c.forwardedDomainsCache == nil {
+		c.forwardedDomainsCache = newForwardedDomainsCacheImpl()
+	}
+	return c.forwardedDomainsCache
 }
 
 func (c *onlyZonesCache) ReportZoneStateConflict(zone DNSHostedZone, err error) bool {
@@ -208,41 +176,18 @@ func (c *onlyZonesCache) Release() {
 
 type defaultZoneCache struct {
 	abstractZonesCache
-	lock     sync.Mutex
-	logger   logger.LogContext
-	metrics  Metrics
-	state    *zoneState
-	persist  bool
-	persistC chan dns.ZoneID
+	lock       sync.Mutex
+	logger     logger.LogContext
+	metrics    Metrics
+	zoneStates *zoneStates
 
 	backoffOnError time.Duration
 }
 
 var _ ZoneCache = &defaultZoneCache{}
 
-func newDefaultZoneCache(common abstractZonesCache, metrics Metrics, handlerData HandlerData) (*defaultZoneCache, error) {
-	state := &zoneState{
-		inMemory:       NewInMemory(),
-		stateTTLGetter: common.config.stateTTLGetter,
-		persistDir:     common.config.persistDir,
-		next:           map[dns.ZoneID]updateTimestamp{},
-		handlerData:    handlerData,
-	}
-	persist := common.config.persistDir != ""
-	cache := &defaultZoneCache{abstractZonesCache: common, logger: common.config.logger, metrics: metrics, state: state, persist: persist}
-	if persist {
-		err := os.MkdirAll(common.config.persistDir, 0777)
-		if err != nil {
-			return nil, fmt.Errorf("creating persistent directory for zone cache at %s failed with %s", common.config.persistDir, err)
-		}
-
-		err = cache.restoreFromDisk()
-		if err != nil {
-			return nil, fmt.Errorf("restoring zone cache from persistent directory %s failed with %s", common.config.persistDir, err)
-		}
-		cache.persistC = make(chan dns.ZoneID)
-		go cache.backgroundWriter()
-	}
+func newDefaultZoneCache(zoneStates *zoneStates, common abstractZonesCache, metrics Metrics) (*defaultZoneCache, error) {
+	cache := &defaultZoneCache{abstractZonesCache: common, logger: common.logger, metrics: metrics, zoneStates: zoneStates}
 	return cache, nil
 }
 
@@ -259,9 +204,9 @@ func (c *defaultZoneCache) GetZones() (DNSHostedZones, error) {
 			c.zonesNext = updateTime.Add(backoff)
 		} else {
 			c.clearBackoff()
-			c.zonesNext = updateTime.Add(c.config.zonesTTL)
+			c.zonesNext = updateTime.Add(c.zonesTTL)
 		}
-		c.state.RestrictCacheToZones(c.zones)
+		c.zoneStates.UpdateUsedZones(c, toSortedZoneIDs(c.zones))
 	} else {
 		c.metrics.AddGenericRequests(M_CACHED_GETZONES, 1)
 	}
@@ -270,7 +215,7 @@ func (c *defaultZoneCache) GetZones() (DNSHostedZones, error) {
 
 func (c *defaultZoneCache) nextBackoff() time.Duration {
 	next := c.backoffOnError*5/4 + 2*time.Second
-	maxBackoff := c.config.zonesTTL / 4
+	maxBackoff := c.zonesTTL / 4
 	if next > maxBackoff {
 		next = maxBackoff
 	}
@@ -283,38 +228,28 @@ func (c *defaultZoneCache) clearBackoff() {
 }
 
 func (c *defaultZoneCache) GetZoneState(zone DNSHostedZone) (DNSZoneState, error) {
-	state, cached, err := c.state.GetZoneState(zone, c)
+	state, cached, err := c.zoneStates.GetZoneState(zone, c)
 	if cached {
 		c.metrics.AddZoneRequests(zone.Id().ID, M_CACHED_GETZONESTATE, 1)
-	} else {
-		c.persistZone(zone)
 	}
 	return state, err
 }
 
 func (c *defaultZoneCache) ReportZoneStateConflict(zone DNSHostedZone, err error) bool {
-	return c.state.ReportZoneStateConflict(zone, err)
+	return c.zoneStates.ReportZoneStateConflict(zone.Id(), err)
 }
 
-func (c *defaultZoneCache) persistZone(zone DNSHostedZone) {
-	if c.persist {
-		c.persistC <- zone.Id()
-	}
-}
-
-func (c *defaultZoneCache) deleteZoneState(zone DNSHostedZone) {
-	c.state.DeleteZoneState(zone)
-	c.persistZone(zone)
+func (c *defaultZoneCache) cleanZoneState(zoneID dns.ZoneID) {
+	c.zoneStates.CleanZoneState(zoneID)
 }
 
 func (c *defaultZoneCache) ApplyRequests(logctx logger.LogContext, err error, zone DNSHostedZone, reqs []*ChangeRequest) {
 	if err == nil {
-		c.state.ExecuteRequests(zone, reqs)
-		c.persistZone(zone)
+		c.zoneStates.ExecuteRequests(zone.Id(), reqs)
 	} else {
 		if !errors.IsThrottlingError(err) {
 			logctx.Infof("zone cache discarded because of error during ExecuteRequests")
-			c.deleteZoneState(zone)
+			c.cleanZoneState(zone.Id())
 			metrics.AddZoneCacheDiscarding(zone.Id())
 		} else {
 			logctx.Infof("zone cache untouched (only throttling during ExecuteRequests)")
@@ -322,217 +257,89 @@ func (c *defaultZoneCache) ApplyRequests(logctx logger.LogContext, err error, zo
 	}
 }
 
-func (c *defaultZoneCache) GetHandlerData() HandlerData {
-	return c.state.GetHandlerData()
+func (c *defaultZoneCache) ForwardedDomainsCache() ForwardedDomainsCache {
+	return c.zoneStates.ForwardedDomainsCache()
 }
 
 func (c *defaultZoneCache) Release() {
-	c.state.RestrictCacheToZones(DNSHostedZones{})
-}
-
-func (c *defaultZoneCache) backgroundWriter() {
-	ticker := time.NewTicker(3 * time.Second)
-
-	written := make(chan dns.ZoneID)
-	zonesToWrite := map[dns.ZoneID]bool{}
-	zonesWriting := map[dns.ZoneID]bool{}
-	for {
-		select {
-		case zoneid := <-c.persistC:
-			zonesToWrite[zoneid] = true
-		case zoneid := <-written:
-			zonesWriting[zoneid] = false
-		case <-c.config.context.Done():
-			for zoneid := range zonesToWrite {
-				if !zonesWriting[zoneid] {
-					zonesWriting[zoneid] = true
-					go c.writeZone(zoneid, written)
-				}
-			}
-			ticker.Stop()
-			return
-		case <-ticker.C:
-			for zoneid := range zonesToWrite {
-				if !zonesWriting[zoneid] {
-					zonesWriting[zoneid] = true
-					go c.writeZone(zoneid, written)
-					delete(zonesToWrite, zoneid)
-				}
-			}
-		}
-	}
-}
-
-type PersistentZone struct {
-	ProviderType     string   `json:"providerType"`
-	Key              string   `json:"key"`
-	Id               string   `json:"id"`
-	Domain           string   `json:"domain"`
-	ForwardedDomains []string `json:"forwardedDomains"`
-	IsPrivate        bool     `json:"isPrivate"`
-}
-
-func (z *PersistentZone) ToDNSHostedZone() DNSHostedZone {
-	return NewDNSHostedZone(z.ProviderType, z.Id, z.Domain, z.Key, z.ForwardedDomains, z.IsPrivate)
-}
-
-func NewPersistentZone(zone DNSHostedZone) *PersistentZone {
-	return &PersistentZone{
-		ProviderType:     zone.Id().ProviderType,
-		Id:               zone.Id().ID,
-		Key:              zone.Key(),
-		Domain:           zone.Domain(),
-		ForwardedDomains: zone.ForwardedDomains(),
-		IsPrivate:        zone.IsPrivate(),
-	}
+	c.zoneStates.UpdateUsedZones(c, nil)
 }
 
-type PersistentZoneState struct {
-	Version     string                 `json:"version"`
-	Valid       time.Time              `json:"valid"`
-	Zone        PersistentZone         `json:"zone"`
-	DNSSets     dns.DNSSets            `json:"dnssets,omitempty"`
-	HandlerData *PersistentHandlerData `json:"handlerData,omitempty"`
+type zoneStateProxy struct {
+	lock            sync.Mutex
+	lastUpdateStart time.Time
+	lastUpdateEnd   time.Time
 }
 
-type PersistentHandlerData struct {
-	Name    string `json:"name"`
-	Version string `json:"version"`
-	Value   *runtime.RawExtension
+type zoneStates struct {
+	lock                  sync.Mutex
+	stateTTLGetter        StateTTLGetter
+	inMemory              *InMemory
+	proxies               map[dns.ZoneID]*zoneStateProxy
+	usedZones             map[ZoneCache][]dns.ZoneID
+	forwardedDomainsCache *forwardedDomainsCacheImpl
 }
 
-const TempSuffix = ".~.tmp"
-
-func (c *defaultZoneCache) writeZone(zoneid dns.ZoneID, written chan<- dns.ZoneID) {
-	c.logger.Infof("writing zone cache for %s", zoneid)
-
-	err := c.state.WriteZone(zoneid)
-	if err != nil {
-		c.logger.Warn(err)
+func newZoneStates(stateTTLGetter StateTTLGetter) *zoneStates {
+	return &zoneStates{
+		inMemory:              NewInMemory(),
+		stateTTLGetter:        stateTTLGetter,
+		proxies:               map[dns.ZoneID]*zoneStateProxy{},
+		usedZones:             map[ZoneCache][]dns.ZoneID{},
+		forwardedDomainsCache: newForwardedDomainsCacheImpl(),
 	}
-	written <- zoneid
 }
 
-var alreadyCleanedupOutdated int32
-
-func zoneCacheCleanupOutdated(logger logger.LogContext, dir, prefix string) {
-	// only perform cleanup once and only if cache dir is set
-	if !atomic.CompareAndSwapInt32(&alreadyCleanedupOutdated, 0, 1) || dir == "" {
-		return
-	}
-
-	files, err := ioutil.ReadDir(dir)
-	if err != nil {
-		logger.Warnf("reading cache dir %s failed with %s", dir, err)
-		return
-	}
-	outdated := time.Now().Add(-24 * time.Hour)
-
-outer:
-	for _, file := range files {
-		if file.IsDir() && strings.HasPrefix(file.Name(), prefix) && file.ModTime().Before(outdated) {
-			subDir := filepath.Join(dir, file.Name())
-			subdirFiles, err := ioutil.ReadDir(subDir)
-			if err != nil {
-				continue
-			}
-			for _, subfile := range subdirFiles {
-				if subfile.IsDir() || !subfile.ModTime().Before(outdated) {
-					continue outer
-				}
-				err = os.Remove(filepath.Join(subDir, subfile.Name()))
-			}
-			err = os.Remove(subDir)
-			if err != nil {
-				logger.Warnf("deleting outdated cache dir %s failed with %s", subDir, err)
-			} else {
-				logger.Infof("deleted outdated cache dir %s", subDir)
-			}
-		}
-	}
-}
-
-func (c *defaultZoneCache) restoreFromDisk() error {
-	dir := c.config.persistDir
-	files, err := ioutil.ReadDir(dir)
-	if err != nil {
-		return fmt.Errorf("restoring zone cache from %s failed with %s", dir, err)
-	}
-
-	zones := DNSHostedZones{}
-	for _, file := range files {
-		if file.IsDir() {
-			continue
-		}
-		if strings.HasSuffix(file.Name(), TempSuffix) ||
-			file.ModTime().Add(24*time.Hour).Before(time.Now()) {
-			_ = os.Remove(filepath.Join(dir, file.Name()))
-			continue
-		}
-		zone, err := c.state.ReadZone(file.Name())
-		if err != nil {
-			c.logger.Info(err)
-		}
-		if zone == nil {
-			_ = os.Remove(filepath.Join(dir, file.Name()))
-			continue
-		}
-		zones = append(zones, zone)
-		c.zonesNext = time.Time{} // enforces sync of zones
-		c.zones = zones
+func (s *zoneStates) getProxy(zoneID dns.ZoneID) *zoneStateProxy {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+	proxy := s.proxies[zoneID]
+	if proxy == nil {
+		proxy = &zoneStateProxy{}
+		s.proxies[zoneID] = proxy
 	}
-	return nil
-}
-
-type updateTimestamp struct {
-	updateStart time.Time
-	updateEnd   time.Time
-}
-
-type zoneState struct {
-	lock           sync.Mutex
-	persistDir     string
-	stateTTLGetter StateTTLGetter
-	inMemory       *InMemory
-	next           map[dns.ZoneID]updateTimestamp
-	handlerData    HandlerData
+	return proxy
 }
 
-func (s *zoneState) GetZoneState(zone DNSHostedZone, cache *defaultZoneCache) (DNSZoneState, bool, error) {
-	s.lock.Lock()
-	defer s.lock.Unlock()
+func (s *zoneStates) GetZoneState(zone DNSHostedZone, cache *defaultZoneCache) (DNSZoneState, bool, error) {
+	proxy := s.getProxy(zone.Id())
+	proxy.lock.Lock()
+	defer proxy.lock.Unlock()
 
-	next, ok := s.next[zone.Id()]
 	start := time.Now()
 	ttl := s.stateTTLGetter(zone.Id())
-	if !ok || start.After(next.updateEnd.Add(ttl)) {
+	if start.After(proxy.lastUpdateEnd.Add(ttl)) {
 		state, err := cache.stateUpdater(zone, cache)
 		if err == nil {
-			s.next[zone.Id()] = updateTimestamp{start, time.Now()}
+			proxy.lastUpdateStart = start
+			proxy.lastUpdateEnd = time.Now()
 			s.inMemory.SetZone(zone, state)
 		} else {
-			s.deleteZoneState(zone)
+			s.cleanZoneState(zone.Id(), proxy)
 		}
 		return state, false, err
 	}
 
-	state, _ := s.inMemory.CloneZoneState(zone)
+	state, err := s.inMemory.CloneZoneState(zone)
+	if err != nil {
+		return nil, true, err
+	}
 	return state, true, nil
 }
 
-func (s *zoneState) ReportZoneStateConflict(zone DNSHostedZone, err error) bool {
-	s.lock.Lock()
-	defer s.lock.Unlock()
+func (s *zoneStates) ReportZoneStateConflict(zoneID dns.ZoneID, err error) bool {
+	proxy := s.getProxy(zoneID)
+	proxy.lock.Lock()
+	defer proxy.lock.Unlock()
 
-	next, found := s.next[zone.Id()]
-	if found {
+	if !proxy.lastUpdateStart.IsZero() {
 		ownerConflict, ok := err.(*errors.AlreadyBusyForOwner)
 		if ok {
-			if ownerConflict.EntryCreatedAt.After(next.updateStart) {
+			if ownerConflict.EntryCreatedAt.After(proxy.lastUpdateStart) {
 				// If a DNSEntry ownership is moved to another DNS controller manager (e.g. shoot recreation on another seed)
 				// the zone cache may have stale owner information. In this case the cache is invalidated
 				// if the entry is newer than the last cache refresh.
-				s.deleteZoneState(zone)
+				s.cleanZoneState(zoneID, proxy)
 				return true
 			}
 		}
@@ -540,160 +347,97 @@ func (s *zoneState) ReportZoneStateConflict(zone DNSHostedZone, err error) bool
 	return false
 }
 
-func (s *zoneState) ExecuteRequests(zone DNSHostedZone, reqs []*ChangeRequest) {
-	s.lock.Lock()
-	defer s.lock.Unlock()
+func (s *zoneStates) ExecuteRequests(zoneID dns.ZoneID, reqs []*ChangeRequest) {
+	proxy := s.getProxy(zoneID)
+	proxy.lock.Lock()
+	defer proxy.lock.Unlock()
 
 	var err error
 	nullMetrics := &NullMetrics{}
 	for _, req := range reqs {
-		err = s.inMemory.Apply(zone.Id(), req, nullMetrics)
+		err = s.inMemory.Apply(zoneID, req, nullMetrics)
 		if err != nil {
 			break
 		}
 	}
 
 	if err != nil {
-		s.deleteZoneState(zone)
+		s.cleanZoneState(zoneID, proxy)
 	}
 }
 
-func (s *zoneState) GetHandlerData() HandlerData {
-	return s.handlerData
+func (s *zoneStates) ForwardedDomainsCache() ForwardedDomainsCache {
+	return s.forwardedDomainsCache
 }
 
-func (s *zoneState) DeleteZoneState(zone DNSHostedZone) {
-	s.lock.Lock()
-	defer s.lock.Unlock()
+func (s *zoneStates) CleanZoneState(zoneID dns.ZoneID) {
+	control := s.getProxy(zoneID)
+	control.lock.Lock()
+	defer control.lock.Unlock()
 
-	s.deleteZoneState(zone)
+	s.cleanZoneState(zoneID, control)
 }
 
-func (s *zoneState) deleteZoneState(zone DNSHostedZone) {
-	delete(s.next, zone.Id())
-	s.inMemory.DeleteZone(zone)
-	if s.handlerData != nil {
-		s.handlerData.DeleteZone(zone.Id())
+func (s *zoneStates) cleanZoneState(zoneID dns.ZoneID, proxy *zoneStateProxy) {
+	s.inMemory.DeleteZone(zoneID)
+	if s.forwardedDomainsCache != nil {
+		s.forwardedDomainsCache.DeleteZone(zoneID)
+	}
+	if proxy != nil {
+		var zero time.Time
+		proxy.lastUpdateStart = zero
+		proxy.lastUpdateEnd = zero
 	}
-	go s.deletePersistedZone(zone.Id())
 }
 
-func (s *zoneState) RestrictCacheToZones(zones DNSHostedZones) {
+func (s *zoneStates) UpdateUsedZones(cache ZoneCache, zoneids []dns.ZoneID) {
 	s.lock.Lock()
 	defer s.lock.Unlock()
 
-	obsoleteZoneIds := map[dns.ZoneID]DNSHostedZone{}
-	for _, zone := range s.inMemory.GetZones() {
-		obsoleteZoneIds[zone.Id()] = zone
-	}
-
-	for _, zone := range zones {
-		delete(obsoleteZoneIds, zone.Id())
-	}
-
-	for _, zone := range obsoleteZoneIds {
-		s.deleteZoneState(zone)
-	}
-}
-
-func (s *zoneState) ReadZone(filename string) (DNSHostedZone, error) {
-	jsonFile, err := os.Open(filepath.Join(s.persistDir, filename))
-	if err != nil {
-		return nil, fmt.Errorf("opening zone cache file %s failed with %s", filename, err)
-	}
-	bytes, err := ioutil.ReadAll(jsonFile)
-	if err != nil {
-		return nil, fmt.Errorf("reading zone cache file %s failed with %s", filename, err)
-	}
-
-	persistentState := &PersistentZoneState{}
-	err = json.Unmarshal(bytes, persistentState)
-	if err != nil {
-		return nil, fmt.Errorf("unmarshalling zone cache from file %s failed with %s", filename, err)
-	}
-
-	if persistentState.Version != "1" {
-		return nil, fmt.Errorf("invalid version %s for zone cache from file %s", persistentState.Version, filename)
-	}
-	if time.Now().After(persistentState.Valid) {
-		return nil, nil
-	}
-
-	return s.RestoreZone(persistentState), nil
-}
-
-func (s *zoneState) buildFilename(zoneid dns.ZoneID) string {
-	return filepath.Join(s.persistDir, strings.Replace(zoneid.ProviderType+"--"+zoneid.ID, "/", "_", -1))
-}
-
-func (s *zoneState) WriteZone(zoneid dns.ZoneID) error {
-	state := s.BuildPersistentZoneState(zoneid)
-	if state == nil {
-		s.deletePersistedZone(zoneid)
-		return nil
+	oldids := s.usedZones[cache]
+	if len(zoneids) == 0 {
+		if len(oldids) == 0 {
+			return
+		}
+		delete(s.usedZones, cache)
+	} else {
+		if reflect.DeepEqual(oldids, zoneids) {
+			return
+		}
+		s.usedZones[cache] = zoneids
 	}
 
-	data, err := json.MarshalIndent(state, "", " ")
-	if err != nil {
-		return fmt.Errorf("marshalling zone cache for %s failed with %s", zoneid, err)
+	allUsed := map[dns.ZoneID]struct{}{}
+	for _, zoneids := range s.usedZones {
+		for _, id := range zoneids {
+			allUsed[id] = struct{}{}
+		}
 	}
 
-	filename := s.buildFilename(zoneid)
-	tmpName := filename + TempSuffix
-	err = ioutil.WriteFile(tmpName, data, 0644)
-	if err != nil {
-		return fmt.Errorf("writing zone cache for %s failed with %s", zoneid, err)
+	for _, zone := range s.inMemory.GetZones() {
+		if _, ok := allUsed[zone.Id()]; !ok {
+			s.cleanZoneState(zone.Id(), nil)
+		}
 	}
-
-	err = os.Rename(tmpName, filename)
-	if err != nil {
-		return fmt.Errorf("renaming zone cache for %s failed with %s", zoneid, err)
+	for id := range s.proxies {
+		if _, ok := allUsed[id]; !ok {
+			delete(s.proxies, id)
+		}
 	}
-
-	return nil
-}
-
-func (s *zoneState) deletePersistedZone(zoneid dns.ZoneID) error {
-	filename := s.buildFilename(zoneid)
-	return os.Remove(filename)
 }
 
-func (s *zoneState) BuildPersistentZoneState(zoneid dns.ZoneID) *PersistentZoneState {
-	s.lock.Lock()
-	defer s.lock.Unlock()
-
-	zone := s.inMemory.FindHostedZone(zoneid)
-	if zone == nil {
+func toSortedZoneIDs(zones DNSHostedZones) []dns.ZoneID {
+	if len(zones) == 0 {
 		return nil
 	}
-	persistentState := &PersistentZoneState{
-		Version: "1",
-		Zone:    *NewPersistentZone(zone),
-		Valid:   s.next[zoneid].updateEnd,
-	}
-
-	state, err := s.inMemory.CloneZoneState(zone)
-	if err == nil {
-		persistentState.DNSSets = state.GetDNSSets()
-	}
-
-	hd := s.GetHandlerData()
-	if hd != nil {
-		value, _ := hd.Marshal(zone.Id())
-		persistentState.HandlerData = value
-	}
-
-	return persistentState
-}
-
-func (s *zoneState) RestoreZone(persistentState *PersistentZoneState) DNSHostedZone {
-	zone := persistentState.Zone.ToDNSHostedZone()
-	zoneState := NewDNSZoneState(persistentState.DNSSets)
-	s.inMemory.SetZone(zone, zoneState)
-	ttl := s.stateTTLGetter(zone.Id())
-	s.next[zone.Id()] = updateTimestamp{persistentState.Valid, persistentState.Valid.Add(ttl)}
-	if persistentState.HandlerData != nil && s.GetHandlerData() != nil {
-		_ = s.GetHandlerData().Unmarshal(zone.Id(), persistentState.HandlerData)
+	zoneids := make([]dns.ZoneID, len(zones))
+	for i, zone := range zones {
+		zoneids[i] = zone.Id()
 	}
-	return zone
+	sort.Slice(zoneids, func(i, j int) bool {
+		cmp1 := strings.Compare(zoneids[i].ProviderType, zoneids[j].ProviderType)
+		cmp2 := strings.Compare(zoneids[i].ID, zoneids[j].ID)
+		return cmp1 < 0 || cmp1 == 0 && cmp2 < 0
+	})
+	return zoneids
 }


Test patch:
diff --git a/pkg/controller/provider/openstack/handler_test.go b/pkg/controller/provider/openstack/handler_test.go
index 001690fb..26716777 100644
--- a/pkg/controller/provider/openstack/handler_test.go
+++ b/pkg/controller/provider/openstack/handler_test.go
@@ -198,8 +198,8 @@ func newMockHandler(mockZones ...*zones.Zone) *Handler {
 		},
 	}
 
-	cacheConfig := provider.NewTestZoneCacheConfig(60*time.Second, 0*time.Second)
-	cache, _ := provider.NewZoneCache(*cacheConfig, mockMetrics, nil, h.getZones, h.getZoneState)
+	cacheFactory := provider.NewTestZoneCacheFactory(60*time.Second, 0*time.Second)
+	cache, _ := cacheFactory.CreateZoneCache(provider.CacheZoneState, mockMetrics, h.getZones, h.getZoneState)
 	h.cache = cache
 	h.config.Options = &provider.FactoryOptions{
 		GenericFactoryOptions: provider.GenericFactoryOptions{},