This is an automated email from the ASF dual-hosted git repository.

dianfu pushed a commit to branch release-2.4
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/release-2.4 by this push:
     new ca602e4c1fc [FLINK-34625][python] Fix state TTL read cache disabling 
(#29576)
ca602e4c1fc is described below

commit ca602e4c1fca5d3dfc920b13555fd43aa558a257
Author: wuyifan <[email protected]>
AuthorDate: Mon Oct 26 15:40:16 2026 +0800

    [FLINK-34625][python] Fix state TTL read cache disabling (#29576)
    
    Use per-state uncached handlers for TTL policies that require backend reads,
    preserving other states' shared caches and the existing write-cache policy.
    Merge pending MapState mutations in empty checks without flushing active
    iterators' caches. Query backend emptiness without TTL cleanup or timestamp
    refresh, excluding pending deleted keys and retaining TTL visibility.
    Restore the request's key context without record-triggered cleanup while
    preserving storage key prefixes across the supported backends.
    Cover mixed expiration, paginated iterator continuation, pending mutations,
    shared-cache isolation and backend key switching with regression tests.
---
 flink-python/pyflink/fn_execution/state_impl.py    |  84 +++-
 .../pyflink/fn_execution/tests/test_state_impl.py  | 436 +++++++++++++++++++++
 .../python/beam/state/BeamKeyedStateStore.java     |  17 +-
 .../python/beam/state/BeamMapStateHandler.java     |  44 +++
 .../streaming/api/utils/PythonOperatorUtils.java   |  12 +-
 .../python/beam/state/BeamMapStateHandlerTest.java | 425 ++++++++++++++++++++
 .../runtime/state/AbstractKeyedStateBackend.java   |   7 +
 .../flink/runtime/state/KeyedStateBackend.java     |   9 +
 .../runtime/state/internal/InternalMapState.java   |  24 +-
 .../state/metrics/MetricsTrackingMapState.java     |  17 +
 .../flink/runtime/state/ttl/TtlMapState.java       |  19 +
 .../flink/runtime/state/StateBackendTestBase.java  |  31 ++
 .../changelog/ChangelogKeyedStateBackend.java      |   5 +
 .../restore/ChangelogMigrationRestoreTarget.java   |   5 +
 .../forst/sync/ForStSyncKeyedStateBackend.java     |   6 +
 .../state/rocksdb/RocksDBKeyedStateBackend.java    |   6 +
 16 files changed, 1122 insertions(+), 25 deletions(-)

diff --git a/flink-python/pyflink/fn_execution/state_impl.py 
b/flink-python/pyflink/fn_execution/state_impl.py
index 257c6e2c118..5e79f50f97b 100644
--- a/flink-python/pyflink/fn_execution/state_impl.py
+++ b/flink-python/pyflink/fn_execution/state_impl.py
@@ -21,6 +21,8 @@ from abc import ABC, abstractmethod
 from apache_beam.coders import coder_impl
 from apache_beam.portability.api import beam_fn_api_pb2
 from apache_beam.runners.worker.bundle_processor import 
SynchronousBagRuntimeState
+from apache_beam.runners.worker.sdk_worker import GlobalCachingStateHandler
+from apache_beam.runners.worker.statecache import StateCache
 from apache_beam.transforms import userstate
 from enum import Enum
 from functools import partial
@@ -120,10 +122,6 @@ class SynchronousKvRuntimeState(InternalKvState, ABC):
         elif ttl_config.get_update_type() == 
StateTtlConfig.UpdateType.OnReadAndWrite:
             self._cache_type = 
SynchronousKvRuntimeState.CacheType.ENABLE_WRITE_CACHE
 
-        if self._cache_type != 
SynchronousKvRuntimeState.CacheType.ENABLE_READ_WRITE_CACHE:
-            # disable read cache
-            
self._remote_state_backend._state_handler._state_cache._cache._max_entries = 0
-
     @abstractmethod
     def get_internal_state(self):
         pass
@@ -145,7 +143,7 @@ class 
SynchronousBagKvRuntimeState(SynchronousKvRuntimeState, ABC):
     def get_internal_state(self):
         if self._internal_state is None:
             self._internal_state = 
self._remote_state_backend._get_internal_bag_state(
-                self.name, self.namespace, self._value_coder, self._ttl_config)
+                self.name, self.namespace, self._value_coder, 
self._ttl_config, self._cache_type)
         return self._internal_state
 
     def _maybe_clear_write_cache(self):
@@ -365,6 +363,7 @@ class CachingMapStateHandler(object):
     GET_FLAG = 0
     ITERATE_FLAG = 1
     CHECK_EMPTY_FLAG = 2
+    CHECK_EMPTY_EXCLUDING_KEYS_FLAG = 3
     # GET response flags
     EXIST_FLAG = 0
     IS_NONE_FLAG = 1
@@ -555,6 +554,18 @@ class CachingMapStateHandler(object):
                     return False
         return self._check_empty_raw(state_key)
 
+    def check_empty_excluding(self, state_key, excluded_keys, map_key_encoder):
+        output_stream = coder_impl.create_OutputStream()
+        output_stream.write_byte(self.CHECK_EMPTY_EXCLUDING_KEYS_FLAG)
+        output_stream.write_bigendian_int32(len(excluded_keys))
+        for key in excluded_keys:
+            key_stream = coder_impl.create_OutputStream()
+            map_key_encoder(key, key_stream)
+            encoded_key = key_stream.get()
+            output_stream.write_bigendian_int32(len(encoded_key))
+            output_stream.write(encoded_key)
+        return self._check_empty_raw(state_key, output_stream.get())
+
     def clear(self, state_key):
         self.clear_read_cache(state_key)
         return self._underlying.clear(state_key)
@@ -577,10 +588,11 @@ class CachingMapStateHandler(object):
     def reset_cached_iterators_num(self):
         self._cached_iterator_num = 0
 
-    def _check_empty_raw(self, state_key):
-        output_stream = coder_impl.create_OutputStream()
-        output_stream.write_byte(self.CHECK_EMPTY_FLAG)
-        continuation_token = output_stream.get()
+    def _check_empty_raw(self, state_key, continuation_token=None):
+        if continuation_token is None:
+            output_stream = coder_impl.create_OutputStream()
+            output_stream.write_byte(self.CHECK_EMPTY_FLAG)
+            continuation_token = output_stream.get()
         data, response_token = self._underlying.get_raw(state_key, 
continuation_token)
         if data[0] == self.IS_EMPTY_FLAG:
             return True
@@ -736,8 +748,10 @@ class InternalSynchronousMapRuntimeState(object):
                  state_key,
                  map_key_coder,
                  map_value_coder,
-                 max_write_cache_entries):
+                 max_write_cache_entries,
+                 read_cache_enabled=True):
         self._map_state_handler = map_state_handler
+        self._read_cache_enabled = read_cache_enabled
         self._state_key = state_key
         self._map_key_coder = map_key_coder
         if isinstance(map_key_coder, FieldCoder):
@@ -815,9 +829,22 @@ class InternalSynchronousMapRuntimeState(object):
             return True
 
     def is_empty(self):
-        if self._is_empty is None:
-            if len(self._write_cache) > 0:
-                self.commit()
+        if self._is_empty:
+            return True
+        if self._write_cache:
+            if any(exists for exists, _ in self._write_cache.values()):
+                self._is_empty = False
+            elif self._cleared:
+                self._is_empty = True
+            else:
+                self._is_empty = self._map_state_handler.check_empty_excluding(
+                    self._state_key, self._write_cache, self._map_key_encoder)
+        elif self._cleared:
+            self._is_empty = True
+        elif not self._read_cache_enabled:
+            self._is_empty = self._map_state_handler.check_empty_excluding(
+                self._state_key, (), self._map_key_encoder)
+        elif self._is_empty is None:
             self._is_empty = 
self._map_state_handler.check_empty(self._state_key)
         return self._is_empty
 
@@ -960,6 +987,11 @@ class RemoteKeyedStateBackend(object):
         self._state_handler = state_handler
         self._map_state_handler = CachingMapStateHandler(
             state_handler, map_state_read_cache_size)
+        # TTL reads must reach the state backend without disabling other 
states' caches.
+        self._uncached_state_handler = GlobalCachingStateHandler(
+            StateCache(0), state_handler._underlying)
+        self._uncached_map_state_handler = CachingMapStateHandler(
+            self._uncached_state_handler, map_state_read_cache_size)
         self._key_coder_impl = key_coder.get_impl()
         self.namespace_coder = namespace_coder
         if namespace_coder:
@@ -1052,7 +1084,7 @@ class RemoteKeyedStateBackend(object):
         self._all_states[name] = wrapped_state
         return wrapped_state
 
-    def _get_internal_bag_state(self, name, namespace, element_coder, 
ttl_config):
+    def _get_internal_bag_state(self, name, namespace, element_coder, 
ttl_config, cache_type):
         encoded_namespace = self._encode_namespace(namespace)
         cached_state = self._internal_state_cache.get(
             (name, self._encoded_current_key, encoded_namespace))
@@ -1065,7 +1097,8 @@ class RemoteKeyedStateBackend(object):
         if isinstance(element_coder, FieldCoder):
             element_coder = FlinkCoder(element_coder)
         state_spec = userstate.BagStateSpec(name, element_coder)
-        internal_state = self._create_bag_state(state_spec, encoded_namespace, 
ttl_config)
+        internal_state = self._create_bag_state(
+            state_spec, encoded_namespace, ttl_config, cache_type)
         return internal_state
 
     def _get_internal_map_state(
@@ -1079,11 +1112,15 @@ class RemoteKeyedStateBackend(object):
             name, encoded_namespace, map_key_coder, map_value_coder, 
ttl_config, cache_type)
         return internal_map_state
 
-    def _create_bag_state(self, state_spec: userstate.StateSpec, 
encoded_namespace, ttl_config) \
+    def _create_bag_state(
+            self, state_spec: userstate.StateSpec, encoded_namespace, 
ttl_config, cache_type) \
             -> userstate.AccumulatingRuntimeState:
         if isinstance(state_spec, userstate.BagStateSpec):
+            state_handler = self._state_handler
+            if cache_type != 
SynchronousKvRuntimeState.CacheType.ENABLE_READ_WRITE_CACHE:
+                state_handler = self._uncached_state_handler
             bag_state = SynchronousBagRuntimeState(
-                self._state_handler,
+                state_handler,
                 state_key=self.get_bag_state_key(
                     state_spec.name, self._encoded_current_key, 
encoded_namespace, ttl_config),
                 value_coder=state_spec.coder)
@@ -1109,12 +1146,18 @@ class RemoteKeyedStateBackend(object):
             write_cache_size = 0
         else:
             write_cache_size = self._map_state_write_cache_size
+        read_cache_enabled = \
+            cache_type == 
SynchronousKvRuntimeState.CacheType.ENABLE_READ_WRITE_CACHE
+        map_state_handler = self._map_state_handler
+        if not read_cache_enabled:
+            map_state_handler = self._uncached_map_state_handler
         return InternalSynchronousMapRuntimeState(
-            self._map_state_handler,
+            map_state_handler,
             state_key,
             map_key_coder,
             map_value_coder,
-            write_cache_size)
+            write_cache_size,
+            read_cache_enabled)
 
     def _encode_namespace(self, namespace):
         if namespace is not None:
@@ -1154,7 +1197,8 @@ class RemoteKeyedStateBackend(object):
                 self.commit_internal_state(state._internal_state)
 
     def clear_cached_iterators(self):
-        if self._map_state_handler.get_cached_iterators_num() > 0:
+        if self._map_state_handler.get_cached_iterators_num() > 0 or \
+                self._uncached_map_state_handler.get_cached_iterators_num() > 
0:
             self._clear_iterator_mark.multimap_side_input.key = 
self._encoded_current_key
             self._map_state_handler.clear(self._clear_iterator_mark)
 
diff --git a/flink-python/pyflink/fn_execution/tests/test_state_impl.py 
b/flink-python/pyflink/fn_execution/tests/test_state_impl.py
new file mode 100644
index 00000000000..73a3f2ada5b
--- /dev/null
+++ b/flink-python/pyflink/fn_execution/tests/test_state_impl.py
@@ -0,0 +1,436 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+
+import contextlib
+import unittest
+from unittest import mock
+
+from apache_beam.coders import VarIntCoder, StrUtf8Coder
+from apache_beam.runners.worker.sdk_worker import GlobalCachingStateHandler, 
StateHandler
+from apache_beam.runners.worker.statecache import StateCache
+
+from pyflink.common import Time, Types
+from pyflink.datastream.state import StateTtlConfig, MapStateDescriptor
+from pyflink.fn_execution.state_impl import RemoteKeyedStateBackend, 
RemoteOperatorStateBackend, \
+    CachingMapStateHandler
+
+
+class StateTtlReadCacheTests(unittest.TestCase):
+
+    @staticmethod
+    def _ttl_configs():
+        for update_type in (StateTtlConfig.UpdateType.OnCreateAndWrite,
+                            StateTtlConfig.UpdateType.OnReadAndWrite):
+            yield StateTtlConfig.new_builder(Time.seconds(5)) \
+                .set_update_type(update_type) \
+                
.set_state_visibility(StateTtlConfig.StateVisibility.NeverReturnExpired) \
+                .build()
+        yield StateTtlConfig.new_builder(Time.seconds(5)) \
+            .update_ttl_on_read_and_write() \
+            
.set_state_visibility(StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp)
 \
+            .build()
+
+    @staticmethod
+    def _create_backend(cache_size=1 << 20):
+        # Only remote I/O is mocked. Both Beam caches and Flink runtime states 
are real.
+        remote = mock.Mock(spec=StateHandler)
+        remote.process_instruction_id.return_value = contextlib.nullcontext()
+        remote.append_raw.return_value = None
+        remote.clear.return_value = None
+        handler = GlobalCachingStateHandler(StateCache(cache_size), remote)
+        backend = RemoteKeyedStateBackend(
+            handler, StrUtf8Coder(), StrUtf8Coder(), 16, 16, 16)
+        backend.set_current_key('key')
+        return backend, handler, remote
+
+    @staticmethod
+    def _encode(value):
+        return VarIntCoder().get_impl().encode_nested(value)
+
+    @classmethod
+    def _map_entry(cls, key, value):
+        return cls._encode(key) + b'\x01' + cls._encode(value)
+
+    @classmethod
+    def _check_empty_excluding_request(cls, *keys):
+        encoded_keys = [cls._encode(key) for key in keys]
+        return b'\x03' + len(keys).to_bytes(4, 'big') + b''.join(
+            len(key).to_bytes(4, 'big') + key for key in encoded_keys)
+
+    def 
test_value_ttl_bypasses_read_cache_without_invalidating_other_states(self):
+        for ttl in self._ttl_configs():
+            with self.subTest(ttl=ttl):
+                backend, handler, remote = self._create_backend()
+                cache = handler._state_cache
+                remote.get_raw.side_effect = [
+                    (self._encode(42), b''), (self._encode(100), b''), (b'', 
b'')]
+                with handler.process_instruction_id('bundle', []):
+                    cached = backend.get_value_state('cached', VarIntCoder())
+                    self.assertEqual(cached.value(), 42)
+                    state = backend.get_value_state('ttl', VarIntCoder(), ttl)
+                    self.assertEqual(state.value(), 100)
+                    # An empty backend response represents expiration, without 
a timing dependency.
+                    self.assertIsNone(state.value())
+                    self.assertEqual(cached.value(), 42)
+                self.assertEqual(remote.get_raw.call_count, 3)
+                self.assertIs(backend._state_handler, handler)
+                self.assertIs(handler._state_cache, cache)
+                self.assertTrue(cache.is_cache_enabled())
+                self.assertIs(cached.get_internal_state()._state_handler, 
handler)
+                self.assertIsNot(state.get_internal_state()._state_handler, 
handler)
+
+    def test_cache_permitting_states_keep_read_cache(self):
+        ttl = StateTtlConfig.new_builder(Time.seconds(5)) \
+            
.set_state_visibility(StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp)
 \
+            .build()
+        for config in (None, ttl):
+            with self.subTest(ttl=config):
+                backend, handler, remote = self._create_backend()
+                remote.get_raw.side_effect = [(self._encode(100), b'')]
+                with handler.process_instruction_id('bundle', []):
+                    state = backend.get_value_state('value', VarIntCoder(), 
config)
+                    self.assertEqual(state.value(), 100)
+                    self.assertEqual(state.value(), 100)
+                remote.get_raw.assert_called_once()
+                self.assertIs(state.get_internal_state()._state_handler, 
handler)
+
+    def test_ttl_write_cache_policy_is_preserved(self):
+        for ttl in self._ttl_configs():
+            with self.subTest(ttl=ttl):
+                backend, handler, remote = self._create_backend()
+                with handler.process_instruction_id('bundle', []):
+                    value = backend.get_value_state('value', VarIntCoder(), 
ttl)
+                    map_state = backend.get_map_state('map', VarIntCoder(), 
VarIntCoder(), ttl)
+                    value.update(100)
+                    map_state.put(1, 100)
+                    if ttl.get_state_visibility() == \
+                            StateTtlConfig.StateVisibility.NeverReturnExpired:
+                        self.assertEqual(remote.append_raw.call_count, 2)
+                    else:
+                        remote.append_raw.assert_not_called()
+                    backend.commit()
+                    self.assertEqual(remote.append_raw.call_count, 2)
+
+    def 
test_map_ttl_bypasses_read_cache_without_invalidating_other_states(self):
+        for ttl in self._ttl_configs():
+            with self.subTest(ttl=ttl):
+                backend, handler, remote = self._create_backend()
+                exists = bytes([CachingMapStateHandler.EXIST_FLAG])
+                missing = bytes([CachingMapStateHandler.NOT_EXIST_FLAG])
+                remote.get_raw.side_effect = [
+                    (exists + self._encode(42), b''),
+                    (exists + self._encode(100), b''), (missing, b'')]
+                with handler.process_instruction_id('bundle', []):
+                    cached = backend.get_map_state('cached', VarIntCoder(), 
VarIntCoder())
+                    self.assertEqual(cached.get(1), 42)
+                    state = backend.get_map_state('ttl', VarIntCoder(), 
VarIntCoder(), ttl)
+                    self.assertEqual(state.get(1), 100)
+                    self.assertIsNone(state.get(1))
+                    self.assertEqual(cached.get(1), 42)
+                self.assertEqual(remote.get_raw.call_count, 3)
+                
self.assertIs(cached.get_internal_state()._map_state_handler._state_cache,
+                              handler._state_cache)
+                self.assertFalse(state.get_internal_state()._map_state_handler
+                                 ._state_cache.is_cache_enabled())
+
+    def test_map_ttl_is_empty_after_committed_value_expires(self):
+        for ttl in self._ttl_configs():
+            with self.subTest(ttl=ttl):
+                backend, handler, remote = self._create_backend()
+                remote.get_raw.side_effect = [
+                    (bytes([CachingMapStateHandler.NOT_EXIST_FLAG]), b''),
+                    (bytes([CachingMapStateHandler.IS_EMPTY_FLAG]), b'')]
+                with handler.process_instruction_id('bundle', []):
+                    state = backend.get_map_state('ttl', VarIntCoder(), 
VarIntCoder(), ttl)
+                    state.put(1, 100)
+                    backend.commit()
+                    self.assertIsNone(state.get(1))
+                    self.assertTrue(state.is_empty())
+                self.assertEqual(remote.get_raw.call_count, 2)
+
+    def test_map_ttl_is_empty_refreshes_within_bundle(self):
+        for ttl in self._ttl_configs():
+            with self.subTest(ttl=ttl):
+                backend, handler, remote = self._create_backend()
+                responses = [
+                    (bytes([CachingMapStateHandler.NOT_EMPTY_FLAG]), b''),
+                    (bytes([CachingMapStateHandler.IS_EMPTY_FLAG]), b'')]
+                writes_are_uncached = ttl.get_state_visibility() == \
+                    StateTtlConfig.StateVisibility.NeverReturnExpired
+                if writes_are_uncached:
+                    
responses.append((bytes([CachingMapStateHandler.NOT_EMPTY_FLAG]), b''))
+                remote.get_raw.side_effect = responses
+                with handler.process_instruction_id('bundle', []):
+                    state = backend.get_map_state('ttl', VarIntCoder(), 
VarIntCoder(), ttl)
+                    mod_count = state.get_internal_state()._mod_count
+                    self.assertFalse(state.is_empty())
+                    self.assertTrue(state.is_empty())
+                    self.assertEqual(state.get_internal_state()._mod_count, 
mod_count)
+                    self.assertTrue(state.is_empty())
+                    self.assertEqual(remote.get_raw.call_count, 2)
+                    for call in remote.get_raw.call_args_list:
+                        self.assertEqual(call.args[1], 
self._check_empty_excluding_request())
+                    state.put(1, 100)
+                    self.assertFalse(state.is_empty())
+                self.assertEqual(remote.get_raw.call_count, 3 if 
writes_are_uncached else 2)
+
+    def test_map_is_empty_merges_pending_mutations(self):
+        ttl = list(self._ttl_configs())[-1]
+        for config in (None, ttl):
+            for operation in ('put', 'remove', 'clear_then_put', 'clear'):
+                with self.subTest(ttl=config, operation=operation):
+                    backend, handler, remote = self._create_backend()
+                    with handler.process_instruction_id('bundle', []):
+                        state = backend.get_map_state('map', VarIntCoder(), 
VarIntCoder(), config)
+                        if operation == 'remove':
+                            state.put(1, 100)
+                            backend.commit()
+                            remote.reset_mock()
+                            state.remove(1)
+                            remote.get_raw.return_value = (
+                                bytes([CachingMapStateHandler.IS_EMPTY_FLAG]), 
b'')
+                        elif operation == 'clear':
+                            state.clear()
+                        else:
+                            if operation == 'clear_then_put':
+                                state.clear()
+                            state.put(1, 100)
+                        self.assertEqual(state.is_empty(), operation in 
('remove', 'clear'))
+                        remote.append_raw.assert_not_called()
+                        remote.clear.assert_not_called()
+                        if operation == 'remove':
+                            remote.get_raw.assert_called_once()
+                            self.assertEqual(remote.get_raw.call_args.args[1],
+                                             
self._check_empty_excluding_request(1))
+                        else:
+                            remote.get_raw.assert_not_called()
+
+    def test_map_ttl_is_empty_preserves_buffered_iterators(self):
+        ttl = list(self._ttl_configs())[-1]
+        expected = {'items': [(1, 100), (2, 200)], 'keys': [1, 2], 'values': 
[100, 200]}
+        for method in expected:
+            for cleared, remove in ((False, False), (False, True), (True, 
False)):
+                with self.subTest(method=method, cleared=cleared, 
remove=remove):
+                    backend, handler, remote = self._create_backend()
+                    remote.get_raw.side_effect = lambda _, request: (
+                        bytes([CachingMapStateHandler.IS_EMPTY_FLAG]) if 
request[0] == 3 else b'',
+                        b'')
+                    with handler.process_instruction_id('bundle', []):
+                        state = backend.get_map_state('ttl', VarIntCoder(), 
VarIntCoder(), ttl)
+                        if cleared:
+                            state.clear()
+                        state.put(1, 100)
+                        state.put(2, 200)
+                        iterator = getattr(state, method)()
+                        self.assertEqual(next(iterator), expected[method][0])
+                        if remove:
+                            iterator.remove()
+                        self.assertFalse(state.is_empty())
+                        self.assertEqual(next(iterator), expected[method][1])
+                        if remove:
+                            iterator.remove()
+                            self.assertTrue(state.is_empty())
+                        self.assertEqual(list(iterator), [])
+                    remote.append_raw.assert_not_called()
+                    remote.clear.assert_not_called()
+
+    def test_map_ttl_iterator_still_rejects_external_mutations(self):
+        ttl = list(self._ttl_configs())[-1]
+        for method in ('items', 'keys', 'values'):
+            for operation in ('put', 'remove', 'clear'):
+                with self.subTest(method=method, operation=operation):
+                    backend, handler, remote = self._create_backend()
+                    remote.get_raw.return_value = (b'', b'')
+                    with handler.process_instruction_id('bundle', []):
+                        state = backend.get_map_state('ttl', VarIntCoder(), 
VarIntCoder(), ttl)
+                        state.put(1, 100)
+                        state.put(2, 200)
+                        iterator = getattr(state, method)()
+                        next(iterator)
+                        if operation == 'put':
+                            state.put(3, 300)
+                        elif operation == 'remove':
+                            state.remove(2)
+                        else:
+                            state.clear()
+                        with self.assertRaisesRegex(Exception, 'Concurrent 
modification detected'):
+                            next(iterator)
+
+    def test_map_ttl_is_empty_excludes_pending_deletions(self):
+        ttl = list(self._ttl_configs())[-1]
+        for backend_keys in ((1,), (1, 2)):
+            with self.subTest(backend_keys=backend_keys):
+                backend, handler, remote = self._create_backend()
+                response = CachingMapStateHandler.IS_EMPTY_FLAG if 
backend_keys == (1,) else \
+                    CachingMapStateHandler.NOT_EMPTY_FLAG
+                remote.get_raw.return_value = (bytes([response]), b'')
+                with handler.process_instruction_id('bundle', []):
+                    state = backend.get_map_state('ttl', VarIntCoder(), 
VarIntCoder(), ttl)
+                    state.remove(1)
+                    self.assertEqual(state.is_empty(), backend_keys == (1,))
+                remote.append_raw.assert_not_called()
+                remote.get_raw.assert_called_once()
+                self.assertEqual(remote.get_raw.call_args.args[1],
+                                 self._check_empty_excluding_request(1))
+
+    def test_map_ttl_is_empty_preserves_paged_remote_iterators(self):
+        ttl = list(self._ttl_configs())[-1]
+        expected = {'items': [(1, 100), (2, 200)], 'keys': [1, 2], 'values': 
[100, 200]}
+        for method in expected:
+            with self.subTest(method=method):
+                backend, handler, remote = self._create_backend()
+                encode_page = self._encode if method == 'keys' else \
+                    lambda key: self._map_entry(key, key * 100)
+                remote.get_raw.side_effect = [
+                    (encode_page(1), b'original-next'),
+                    (bytes([CachingMapStateHandler.NOT_EMPTY_FLAG]), b''),
+                    (encode_page(2), b'')]
+                with handler.process_instruction_id('bundle', []):
+                    state = backend.get_map_state('ttl', VarIntCoder(), 
VarIntCoder(), ttl)
+                    iterator = getattr(state, method)()
+                    self.assertEqual(next(iterator), expected[method][0])
+                    iterator.remove()
+                    self.assertFalse(state.is_empty())
+                    self.assertEqual(next(iterator), expected[method][1])
+                    self.assertEqual(list(iterator), [])
+                    self.assertEqual(
+                        
backend._uncached_map_state_handler.get_cached_iterators_num(), 0)
+                self.assertEqual(remote.get_raw.call_count, 3)
+                self.assertEqual(remote.get_raw.call_args_list[1].args[1],
+                                 self._check_empty_excluding_request(1))
+                self.assertIn(b'original-next', 
remote.get_raw.call_args_list[2].args[1])
+                remote.append_raw.assert_not_called()
+
+    def test_map_ttl_is_empty_does_not_start_another_ttl_iterator(self):
+        ttl = list(self._ttl_configs())[-1]
+        backend, handler, remote = self._create_backend()
+        remote.get_raw.return_value = (self._map_entry(1, 100), b'active')
+        with handler.process_instruction_id('bundle', []):
+            state = backend.get_map_state('ttl', VarIntCoder(), VarIntCoder(), 
ttl)
+            iterator = state.items()
+            self.assertEqual(next(iterator), (1, 100))
+            iterator.remove()
+            internal = state.get_internal_state()
+            pending = dict(internal._write_cache)
+            mod_count = internal._mod_count
+
+            def check_empty(_, request):
+                # Iterating mixed expired/live backend entries could 
invalidate the active token.
+                self.assertEqual(request, 
self._check_empty_excluding_request(1))
+                return bytes([CachingMapStateHandler.NOT_EMPTY_FLAG]), b''
+
+            remote.get_raw.side_effect = check_empty
+            self.assertFalse(state.is_empty())
+            self.assertEqual(internal._write_cache, pending)
+            self.assertEqual(internal._mod_count, mod_count)
+            
self.assertEqual(backend._uncached_map_state_handler.get_cached_iterators_num(),
 1)
+            remote.append_raw.assert_not_called()
+            remote.clear.assert_not_called()
+            remote.get_raw.side_effect = None
+            remote.get_raw.return_value = (self._map_entry(2, 200), b'')
+            self.assertEqual(list(iterator), [(2, 200)])
+            
self.assertEqual(backend._uncached_map_state_handler.get_cached_iterators_num(),
 0)
+
+    def test_cache_permitting_map_states_keep_is_empty_cache(self):
+        ttl = StateTtlConfig.new_builder(Time.seconds(5)) \
+            
.set_state_visibility(StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp)
 \
+            .build()
+        for config in (None, ttl):
+            with self.subTest(ttl=config):
+                backend, handler, remote = self._create_backend()
+                remote.get_raw.side_effect = [
+                    (bytes([CachingMapStateHandler.NOT_EMPTY_FLAG]), b'')]
+                with handler.process_instruction_id('bundle', []):
+                    state = backend.get_map_state('map', VarIntCoder(), 
VarIntCoder(), config)
+                    self.assertFalse(state.is_empty())
+                    self.assertFalse(state.is_empty())
+                remote.get_raw.assert_called_once()
+                self.assertEqual(remote.get_raw.call_args.args[1],
+                                 
bytes([CachingMapStateHandler.CHECK_EMPTY_FLAG]))
+
+    def test_broadcast_map_state_keeps_default_cache_behavior(self):
+        _, handler, remote = self._create_backend()
+        backend = RemoteOperatorStateBackend(handler, 16, 16, 16)
+        with handler.process_instruction_id('bundle', []):
+            state = backend.get_broadcast_state(
+                MapStateDescriptor('broadcast', Types.INT(), Types.INT()))
+            state.put(1, 100)
+            backend.commit()
+            self.assertFalse(state._internal_map_state.is_empty())
+            self.assertEqual(state.get(1), 100)
+        remote.get_raw.assert_not_called()
+
+    def test_map_ttl_iteration_and_unfinished_iterator_cleanup(self):
+        ttl = next(self._ttl_configs())
+        backend, handler, remote = self._create_backend()
+        remote.get_raw.side_effect = [
+            (self._map_entry(1, 100), b''), (b'', b''),
+            (self._map_entry(1, 100), b'next-page'),
+            (self._map_entry(2, 200), b''),
+            (self._map_entry(1, 100), b'unfinished')]
+        with handler.process_instruction_id('bundle', []):
+            state = backend.get_map_state('ttl', VarIntCoder(), VarIntCoder(), 
ttl)
+            self.assertEqual(list(state.items()), [(1, 100)])
+            self.assertEqual(list(state.items()), [])
+            self.assertEqual(list(state.items()), [(1, 100), (2, 200)])
+            self.assertIn(b'next-page', 
remote.get_raw.call_args_list[3].args[1])
+            self.assertEqual(next(state.items()), (1, 100))
+            
self.assertEqual(backend._map_state_handler.get_cached_iterators_num(), 0)
+            backend.clear_cached_iterators()
+        remote.clear.assert_called_once()
+        clear_key = remote.clear.call_args.args[0].multimap_side_input
+        self.assertEqual(clear_key.transform_id, 'clear_iterators')
+        self.assertEqual(clear_key.key, b'key')
+
+    def test_ttl_handler_is_preserved_when_reusing_key_and_namespace(self):
+        ttl = next(self._ttl_configs())
+        backend, handler, remote = self._create_backend()
+        remote.get_raw.side_effect = [
+            (self._encode(100), b''), (self._encode(200), b''), (b'', b'')]
+        with handler.process_instruction_id('bundle', []):
+            state = backend.get_value_state('ttl', VarIntCoder(), ttl)
+            state.set_current_namespace('first')
+            self.assertEqual(state.value(), 100)
+            first_internal_state = state.get_internal_state()
+            backend.set_current_key('other')
+            state.set_current_namespace('second')
+            self.assertEqual(state.value(), 200)
+            backend.set_current_key('key')
+            state.set_current_namespace('first')
+            self.assertIs(state.get_internal_state(), first_internal_state)
+            self.assertIsNone(state.value())
+        first_key, second_key, reused_key = [
+            call.args[0] for call in remote.get_raw.call_args_list]
+        self.assertEqual(first_key, reused_key)
+        self.assertNotEqual(first_key, second_key)
+
+    def test_repeated_state_registration_with_disabled_cache(self):
+        ttl = next(self._ttl_configs())
+        backend, handler, remote = self._create_backend(cache_size=0)
+        remote.get_raw.side_effect = [(self._encode(100), b''), (b'', b'')]
+        with handler.process_instruction_id('bundle', []):
+            state = backend.get_value_state('ttl', VarIntCoder(), ttl)
+            self.assertEqual(state.value(), 100)
+            self.assertIs(backend.get_value_state('ttl', VarIntCoder(), ttl), 
state)
+            self.assertIsNone(state.value())
+        self.assertEqual(remote.get_raw.call_count, 2)
+        self.assertFalse(handler._state_cache.is_cache_enabled())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git 
a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamKeyedStateStore.java
 
b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamKeyedStateStore.java
index 83981df889b..b50242f3367 100644
--- 
a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamKeyedStateStore.java
+++ 
b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamKeyedStateStore.java
@@ -152,7 +152,12 @@ public class BeamKeyedStateStore implements BeamStateStore 
{
         byte[] keyBytes = mapUserState.getKey().toByteArray();
         bais.setBuffer(keyBytes, 0, keyBytes.length);
         Object key = keySerializer.deserialize(baisWrapper);
-        setCurrentKey(key);
+        boolean readOnly =
+                request.hasGet()
+                        && !request.getGet().getContinuationToken().isEmpty()
+                        && request.getGet().getContinuationToken().byteAt(0)
+                                == 
BeamMapStateHandler.CHECK_EMPTY_EXCLUDING_KEYS_FLAG;
+        setCurrentKey(key, readOnly);
 
         // Retrieve state descriptor
         byte[] data = 
Base64.getDecoder().decode(mapUserState.getSideInputId());
@@ -198,13 +203,19 @@ public class BeamKeyedStateStore implements 
BeamStateStore {
     }
 
     private void setCurrentKey(Object key) {
+        setCurrentKey(key, false);
+    }
+
+    private void setCurrentKey(Object key, boolean forStateAccess) {
         if (keyedStateBackend.getKeySerializer() instanceof RowDataSerializer) 
{
             setCurrentKeyForStreaming(
                     (KeyedStateBackend<? super BinaryRowData>) 
keyedStateBackend,
                     ((RowDataSerializer) keyedStateBackend.getKeySerializer())
-                            .toBinaryRow((RowData) key));
+                            .toBinaryRow((RowData) key),
+                    forStateAccess);
         } else {
-            setCurrentKeyForStreaming((KeyedStateBackend<Object>) 
keyedStateBackend, key);
+            setCurrentKeyForStreaming(
+                    (KeyedStateBackend<Object>) keyedStateBackend, key, 
forStateAccess);
         }
     }
 }
diff --git 
a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamMapStateHandler.java
 
b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamMapStateHandler.java
index 6bc2f7ad8aa..e5075ce67cd 100644
--- 
a/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamMapStateHandler.java
+++ 
b/flink-python/src/main/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamMapStateHandler.java
@@ -25,16 +25,21 @@ import 
org.apache.flink.core.memory.ByteArrayOutputStreamWithPos;
 import org.apache.flink.core.memory.DataInputViewStreamWrapper;
 import org.apache.flink.core.memory.DataOutputViewStreamWrapper;
 import org.apache.flink.python.PythonOptions;
+import org.apache.flink.runtime.state.internal.InternalMapState;
 import org.apache.flink.streaming.api.utils.ByteArrayWrapper;
 
 import org.apache.beam.model.fnexecution.v1.BeamFnApi;
 import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
+import static org.apache.flink.util.Preconditions.checkArgument;
+
 /** BeamMapStateHandler handles operations on a {@link MapState}. */
 public class BeamMapStateHandler
         extends AbstractBeamStateHandler<MapState<ByteArrayWrapper, byte[]>> {
@@ -46,6 +51,7 @@ public class BeamMapStateHandler
 
     private static final byte ITERATE_FLAG = 1;
     private static final byte CHECK_EMPTY_FLAG = 2;
+    static final byte CHECK_EMPTY_EXCLUDING_KEYS_FLAG = 3;
 
     /** Map state GET response flags. */
     private static final byte EXIST_FLAG = 0;
@@ -118,6 +124,8 @@ public class BeamMapStateHandler
         // [flag (1 byte)][serialized map key]
         // The continuation token structure of CHECK_EMPTY request is:
         // [flag (1 byte)]
+        // The continuation token structure of CHECK_EMPTY_EXCLUDING_KEYS 
request is:
+        // [flag (1 byte)][key count (int32)][key length (int32)][serialized 
key]...
         // The continuation token structure of ITERATE request is:
         // [flag (1 byte)][iterate type (1 byte)][iterator token length 
(int32)][iterator token]
         byte[] getRequest = 
request.getGet().getContinuationToken().toByteArray();
@@ -133,6 +141,22 @@ public class BeamMapStateHandler
             case CHECK_EMPTY_FLAG:
                 response = handleCheckEmpty(mapState);
                 break;
+            case CHECK_EMPTY_EXCLUDING_KEYS_FLAG:
+                bais.setBuffer(getRequest, 1, getRequest.length - 1);
+                final int keyCount = baisWrapper.readInt();
+                checkArgument(keyCount >= 0, "The excluded key count must not 
be negative.");
+                final Set<ByteArrayWrapper> excludedKeys = new HashSet<>();
+                for (int i = 0; i < keyCount; i++) {
+                    final int keyLength = baisWrapper.readInt();
+                    checkArgument(
+                            keyLength >= 0 && keyLength <= bais.available(),
+                            "Invalid excluded key length.");
+                    final byte[] key = new byte[keyLength];
+                    baisWrapper.readFully(key);
+                    excludedKeys.add(new ByteArrayWrapper(key));
+                }
+                response = handleCheckEmptyExcludingKeys(mapState, 
excludedKeys);
+                break;
             case ITERATE_FLAG:
                 bais.setBuffer(getRequest, 1, getRequest.length - 1);
                 IterateType iterateType = 
IterateType.fromOrd(baisWrapper.readByte());
@@ -245,6 +269,26 @@ public class BeamMapStateHandler
         }
     }
 
+    private BeamFnApi.StateGetResponse.Builder handleCheckEmptyExcludingKeys(
+            MapState<ByteArrayWrapper, byte[]> mapState, Set<ByteArrayWrapper> 
excludedKeys)
+            throws Exception {
+        if (mapState instanceof InternalMapState<?, ?, ?, ?>) {
+            return ((InternalMapState<?, ?, ?, ?>) 
mapState).isEmptyExcluding(excludedKeys)
+                    ? IS_EMPTY_RESPONSE
+                    : NOT_EMPTY_RESPONSE;
+        }
+        // Operator map state has no TTL decorator.
+        final Iterable<ByteArrayWrapper> keys = mapState.keys();
+        if (keys != null) {
+            for (ByteArrayWrapper key : keys) {
+                if (!excludedKeys.contains(key)) {
+                    return NOT_EMPTY_RESPONSE;
+                }
+            }
+        }
+        return IS_EMPTY_RESPONSE;
+    }
+
     private BeamFnApi.StateGetResponse.Builder handleIterate(
             MapState<ByteArrayWrapper, byte[]> mapState,
             IterateType iterateType,
diff --git 
a/flink-python/src/main/java/org/apache/flink/streaming/api/utils/PythonOperatorUtils.java
 
b/flink-python/src/main/java/org/apache/flink/streaming/api/utils/PythonOperatorUtils.java
index dc26f452ab5..7d657d2969f 100644
--- 
a/flink-python/src/main/java/org/apache/flink/streaming/api/utils/PythonOperatorUtils.java
+++ 
b/flink-python/src/main/java/org/apache/flink/streaming/api/utils/PythonOperatorUtils.java
@@ -31,8 +31,18 @@ public class PythonOperatorUtils {
     /** Set the current key for streaming operator. */
     public static <K> void setCurrentKeyForStreaming(
             KeyedStateBackend<K> stateBackend, K currentKey) {
+        setCurrentKeyForStreaming(stateBackend, currentKey, false);
+    }
+
+    /** Selects a state-request key without treating a read-only query as 
another input record. */
+    public static <K> void setCurrentKeyForStreaming(
+            KeyedStateBackend<K> stateBackend, K currentKey, boolean 
forStateAccess) {
         if (!inBatchExecutionMode(stateBackend)) {
-            stateBackend.setCurrentKey(currentKey);
+            if (forStateAccess) {
+                stateBackend.setCurrentKeyForStateAccess(currentKey);
+            } else {
+                stateBackend.setCurrentKey(currentKey);
+            }
         }
     }
 
diff --git 
a/flink-python/src/test/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamMapStateHandlerTest.java
 
b/flink-python/src/test/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamMapStateHandlerTest.java
new file mode 100644
index 00000000000..b75260c2d7e
--- /dev/null
+++ 
b/flink-python/src/test/java/org/apache/flink/streaming/api/runners/python/beam/state/BeamMapStateHandlerTest.java
@@ -0,0 +1,425 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.streaming.api.runners.python.beam.state;
+
+import org.apache.flink.api.common.state.MapState;
+import org.apache.flink.api.common.state.MapStateDescriptor;
+import org.apache.flink.api.common.state.StateTtlConfig;
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.common.typeutils.base.IntSerializer;
+import 
org.apache.flink.api.common.typeutils.base.array.BytePrimitiveArraySerializer;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.StateLatencyTrackOptions;
+import org.apache.flink.fnexecution.v1.FlinkFnApi.StateDescriptor;
+import 
org.apache.flink.fnexecution.v1.FlinkFnApi.StateDescriptor.StateTTLConfig;
+import 
org.apache.flink.fnexecution.v1.FlinkFnApi.StateDescriptor.StateTTLConfig.CleanupStrategies;
+import org.apache.flink.python.PythonOptions;
+import org.apache.flink.runtime.state.AbstractKeyedStateBackend;
+import org.apache.flink.runtime.state.KeyGroupRangeAssignment;
+import org.apache.flink.runtime.state.VoidNamespace;
+import org.apache.flink.runtime.state.VoidNamespaceSerializer;
+import org.apache.flink.runtime.state.hashmap.HashMapStateBackend;
+import org.apache.flink.streaming.api.operators.StreamMap;
+import org.apache.flink.streaming.api.utils.ByteArrayWrapper;
+import org.apache.flink.streaming.api.utils.ByteArrayWrapperSerializer;
+import org.apache.flink.streaming.util.KeyedOneInputStreamOperatorTestHarness;
+
+import org.apache.beam.model.fnexecution.v1.BeamFnApi;
+import org.apache.beam.vendor.grpc.v1p69p0.com.google.protobuf.ByteString;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests the map-state protocol against a real heap backend and a 
controllable TTL clock. */
+class BeamMapStateHandlerTest {
+
+    @ParameterizedTest
+    @CsvSource({
+        "ITEMS, false, ReturnExpiredIfNotCleanedUp",
+        "KEYS, false, ReturnExpiredIfNotCleanedUp",
+        "VALUES, false, ReturnExpiredIfNotCleanedUp",
+        "ITEMS, true, ReturnExpiredIfNotCleanedUp",
+        "KEYS, true, ReturnExpiredIfNotCleanedUp",
+        "VALUES, true, ReturnExpiredIfNotCleanedUp",
+        "ITEMS, false, NeverReturnExpired",
+        "KEYS, false, NeverReturnExpired",
+        "VALUES, false, NeverReturnExpired",
+        "ITEMS, true, NeverReturnExpired",
+        "KEYS, true, NeverReturnExpired",
+        "VALUES, true, NeverReturnExpired"
+    })
+    void testMixedExpiryDoesNotInvalidateActiveIterator(
+            IterateType type, boolean metrics, StateTtlConfig.StateVisibility 
visibility)
+            throws Exception {
+        try (Fixture fixture = new Fixture(metrics, visibility)) {
+            for (int key = 0; key < 64; key++) {
+                fixture.state.put(key(key), new byte[] {(byte) key});
+            }
+            List<Integer> order = new ArrayList<>();
+            for (ByteArrayWrapper key : fixture.state.keys()) {
+                order.add(Byte.toUnsignedInt(key.get(0)));
+            }
+            fixture.harness.setStateTtlProcessingTime(101);
+            for (int key : order.subList(0, 8)) {
+                fixture.state.put(key(key), new byte[] {(byte) key});
+            }
+
+            BeamFnApi.StateGetResponse page = iterate(fixture, type, 
ByteString.EMPTY);
+            List<Integer> firstPage = decodeKeys(page, type);
+            assertThat(firstPage).containsExactlyElementsOf(order.subList(0, 
2));
+            
assertThat(page.getContinuationToken()).isNotEqualTo(ByteString.EMPTY);
+            assertThat(checkEmptyExcluding(fixture, 
Collections.singletonList(firstPage.get(0))))
+                    .isFalse();
+
+            List<Integer> visited = new ArrayList<>(firstPage);
+            while (!page.getContinuationToken().isEmpty()) {
+                page = iterate(fixture, type, page.getContinuationToken());
+                visited.addAll(decodeKeys(page, type));
+            }
+            // ReturnExpiredIfNotCleanedUp permits expired entries to be 
returned during cleanup.
+            List<Integer> expected =
+                    visibility == 
StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp
+                            ? order
+                            : order.subList(0, 8);
+            assertThat(visited).containsExactlyElementsOf(expected);
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testExcludedKeysAndNullValues(boolean metrics) throws Exception {
+        try (Fixture fixture =
+                new Fixture(metrics, 
StateTtlConfig.StateVisibility.NeverReturnExpired)) {
+            fixture.state.put(key(1), null);
+            fixture.state.put(key(2), new byte[] {2});
+            assertThat(checkEmptyExcluding(fixture, 
Collections.emptyList())).isFalse();
+            assertThat(checkEmptyExcluding(fixture, 
Collections.singletonList(2))).isFalse();
+            assertThat(checkEmptyExcluding(fixture, List.of(1, 2))).isTrue();
+            fixture.state.clear();
+            assertThat(checkEmptyExcluding(fixture, 
Collections.emptyList())).isTrue();
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testCheckingEmptyDoesNotRefreshTtl(boolean metrics) throws Exception {
+        try (Fixture fixture =
+                new Fixture(metrics, 
StateTtlConfig.StateVisibility.NeverReturnExpired)) {
+            fixture.state.put(key(1), new byte[] {1});
+            fixture.harness.setStateTtlProcessingTime(50);
+            assertThat(checkEmptyExcluding(fixture, 
Collections.singletonList(2))).isFalse();
+            fixture.harness.setStateTtlProcessingTime(101);
+            assertThat(fixture.state.get(key(1))).isNull();
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testEmptyExclusionsDoNotRunIncrementalCleanup(boolean metrics) throws 
Exception {
+        try (Fixture fixture =
+                new Fixture(metrics, 
StateTtlConfig.StateVisibility.NeverReturnExpired, true)) {
+            fixture.state.put(key(1), new byte[] {1});
+            fixture.harness.setStateTtlProcessingTime(101);
+            assertThat(checkEmptyExcluding(fixture, 
Collections.emptyList())).isTrue();
+            // A read-only query reports expiration while retaining the 
physical state entry.
+            assertThat(fixture.harness.numKeyedStateEntries()).isEqualTo(1);
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void testReadOnlyRpcDoesNotTriggerPerRecordCleanup(boolean metrics) throws 
Exception {
+        try (Fixture fixture =
+                new Fixture(
+                        metrics, 
StateTtlConfig.StateVisibility.NeverReturnExpired, false, 16)) {
+            BeamStateRequestHandler requests =
+                    BeamStateRequestHandler.of(
+                            fixture.backend, null, IntSerializer.INSTANCE, 
null, fixture.config);
+            AtomicInteger selections = new AtomicInteger();
+            fixture.backend.registerKeySelectionListener(ignored -> 
selections.incrementAndGet());
+            BeamFnApi.StateKey firstKey = rpcStateKey(0);
+            BeamFnApi.StateKey secondKey = rpcStateKey(1);
+            appendThroughStore(requests, firstKey);
+            appendThroughStore(requests, secondKey);
+            BeamFnApi.StateGetResponse page =
+                    getThroughStore(
+                            requests,
+                            firstKey,
+                            iterateRequest(IterateType.ITEMS, 
ByteString.EMPTY));
+            
assertThat(page.getContinuationToken()).isNotEqualTo(ByteString.EMPTY);
+            int beforeQuery = selections.get();
+            assertThat(beforeQuery).isEqualTo(3);
+            fixture.harness.setStateTtlProcessingTime(101);
+
+            for (int key : new int[] {0, 1, 0}) {
+                BeamFnApi.StateGetResponse response =
+                        getThroughStore(
+                                requests,
+                                rpcStateKey(key),
+                                
checkEmptyExcludingRequest(Collections.emptyList()));
+                
assertThat(response.getData()).isEqualTo(ByteString.copyFrom(new byte[] {3}));
+                assertThat(selections.get()).isEqualTo(beforeQuery);
+                assertThat(fixture.backend.getCurrentKey()).isEqualTo(key);
+                assertThat(fixture.backend.getCurrentKeyGroupIndex())
+                        
.isEqualTo(KeyGroupRangeAssignment.assignToKeyGroup(key, 16));
+                
assertThat(fixture.harness.numKeyedStateEntries()).isEqualTo(2);
+            }
+
+            int ordinaryRequests = 0;
+            while (!page.getContinuationToken().isEmpty()) {
+                page =
+                        getThroughStore(
+                                requests,
+                                firstKey,
+                                iterateRequest(IterateType.ITEMS, 
page.getContinuationToken()));
+                ordinaryRequests++;
+            }
+            assertThat(ordinaryRequests).isPositive();
+            assertThat(selections.get()).isEqualTo(beforeQuery + 
ordinaryRequests);
+        }
+    }
+
+    private static BeamFnApi.StateKey rpcStateKey(int key) throws Exception {
+        CleanupStrategies cleanup =
+                CleanupStrategies.newBuilder()
+                        .setIsCleanupInBackground(false)
+                        .addStrategies(
+                                
CleanupStrategies.MapStrategiesEntry.newBuilder()
+                                        .setStrategy(
+                                                
CleanupStrategies.Strategies.INCREMENTAL_CLEANUP)
+                                        .setIncrementalCleanupStrategy(
+                                                
CleanupStrategies.IncrementalCleanupStrategy
+                                                        .newBuilder()
+                                                        .setCleanupSize(1)
+                                                        
.setRunCleanupForEveryRecord(true)))
+                        .build();
+        StateDescriptor descriptor =
+                StateDescriptor.newBuilder()
+                        .setStateName("rpc-map")
+                        .setStateTtlConfig(
+                                StateTTLConfig.newBuilder()
+                                        .setTtl(100)
+                                        
.setUpdateType(StateTTLConfig.UpdateType.OnReadAndWrite)
+                                        .setStateVisibility(
+                                                
StateTTLConfig.StateVisibility.NeverReturnExpired)
+                                        .setCleanupStrategies(cleanup))
+                        .build();
+        ByteArrayOutputStream keyBytes = new ByteArrayOutputStream();
+        new DataOutputStream(keyBytes).writeInt(key);
+        return BeamFnApi.StateKey.newBuilder()
+                .setMultimapSideInput(
+                        BeamFnApi.StateKey.MultimapSideInput.newBuilder()
+                                
.setKey(ByteString.copyFrom(keyBytes.toByteArray()))
+                                .setSideInputId(
+                                        Base64.getEncoder()
+                                                
.encodeToString(descriptor.toByteArray())))
+                .build();
+    }
+
+    private static void appendThroughStore(BeamStateRequestHandler handler, 
BeamFnApi.StateKey key)
+            throws Exception {
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        DataOutputStream data = new DataOutputStream(bytes);
+        data.writeInt(4);
+        for (int mapKey = 0; mapKey < 4; mapKey++) {
+            data.writeByte(2);
+            data.writeInt(1);
+            data.writeByte(mapKey);
+            data.writeInt(1);
+            data.writeByte(mapKey);
+        }
+        handler.handle(
+                        BeamFnApi.StateRequest.newBuilder()
+                                .setId("append")
+                                .setStateKey(key)
+                                .setAppend(
+                                        
BeamFnApi.StateAppendRequest.newBuilder()
+                                                
.setData(ByteString.copyFrom(bytes.toByteArray())))
+                                .build())
+                .toCompletableFuture()
+                .get();
+    }
+
+    private static BeamFnApi.StateGetResponse getThroughStore(
+            BeamStateRequestHandler handler, BeamFnApi.StateKey key, byte[] 
request)
+            throws Exception {
+        return handler.handle(
+                        BeamFnApi.StateRequest.newBuilder()
+                                .setId("get")
+                                .setStateKey(key)
+                                .setGet(
+                                        BeamFnApi.StateGetRequest.newBuilder()
+                                                
.setContinuationToken(ByteString.copyFrom(request)))
+                                .build())
+                .toCompletableFuture()
+                .get()
+                .build()
+                .getGet();
+    }
+
+    private static boolean checkEmptyExcluding(Fixture fixture, List<Integer> 
excluded)
+            throws Exception {
+        BeamFnApi.StateGetResponse response = get(fixture, 
checkEmptyExcludingRequest(excluded));
+        
assertThat(response.getContinuationToken()).isEqualTo(ByteString.EMPTY);
+        assertThat(response.getData().size()).isEqualTo(1);
+        assertThat(response.getData().byteAt(0)).isIn((byte) 3, (byte) 4);
+        return response.getData().byteAt(0) == 3;
+    }
+
+    private static byte[] checkEmptyExcludingRequest(List<Integer> excluded) 
throws Exception {
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        DataOutputStream request = new DataOutputStream(bytes);
+        request.writeByte(3);
+        request.writeInt(excluded.size());
+        for (int key : excluded) {
+            request.writeInt(1);
+            request.writeByte(key);
+        }
+        return bytes.toByteArray();
+    }
+
+    private static BeamFnApi.StateGetResponse iterate(
+            Fixture fixture, IterateType type, ByteString token) throws 
Exception {
+        return get(fixture, iterateRequest(type, token));
+    }
+
+    private static byte[] iterateRequest(IterateType type, ByteString token) 
throws Exception {
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        DataOutputStream request = new DataOutputStream(bytes);
+        request.writeByte(1);
+        request.writeByte(type.getOrd());
+        request.writeInt(token.size());
+        token.writeTo(request);
+        return bytes.toByteArray();
+    }
+
+    private static BeamFnApi.StateGetResponse get(Fixture fixture, byte[] 
request)
+            throws Exception {
+        return fixture.handler
+                .handleGet(
+                        BeamFnApi.StateRequest.newBuilder()
+                                .setId("request")
+                                .setGet(
+                                        BeamFnApi.StateGetRequest.newBuilder()
+                                                
.setContinuationToken(ByteString.copyFrom(request)))
+                                .build(),
+                        fixture.state)
+                .build()
+                .getGet();
+    }
+
+    private static List<Integer> decodeKeys(BeamFnApi.StateGetResponse 
response, IterateType type)
+            throws Exception {
+        DataInputStream input =
+                new DataInputStream(new 
ByteArrayInputStream(response.getData().toByteArray()));
+        List<Integer> keys = new ArrayList<>();
+        while (input.available() > 0) {
+            keys.add(input.readUnsignedByte());
+            if (type != IterateType.KEYS && input.readBoolean()) {
+                input.readByte();
+            }
+        }
+        return keys;
+    }
+
+    private static ByteArrayWrapper key(int key) {
+        return new ByteArrayWrapper(new byte[] {(byte) key});
+    }
+
+    private static class Fixture implements AutoCloseable {
+        private final KeyedOneInputStreamOperatorTestHarness<Integer, Integer, 
Integer> harness;
+        private final MapState<ByteArrayWrapper, byte[]> state;
+        private final BeamMapStateHandler handler;
+        private final AbstractKeyedStateBackend<Integer> backend;
+        private final Configuration config;
+
+        private Fixture(boolean metrics, StateTtlConfig.StateVisibility 
visibility)
+                throws Exception {
+            this(metrics, visibility, false);
+        }
+
+        private Fixture(
+                boolean metrics, StateTtlConfig.StateVisibility visibility, 
boolean incremental)
+                throws Exception {
+            this(metrics, visibility, incremental, 1);
+        }
+
+        private Fixture(
+                boolean metrics,
+                StateTtlConfig.StateVisibility visibility,
+                boolean incremental,
+                int maxParallelism)
+                throws Exception {
+            config = new Configuration();
+            config.set(PythonOptions.MAP_STATE_ITERATE_RESPONSE_BATCH_SIZE, 2);
+            config.set(StateLatencyTrackOptions.LATENCY_TRACK_ENABLED, 
metrics);
+            config.set(StateLatencyTrackOptions.LATENCY_TRACK_SAMPLE_INTERVAL, 
1);
+            StreamMap<Integer, Integer> operator = new StreamMap<>(value -> 
value);
+            harness =
+                    new KeyedOneInputStreamOperatorTestHarness<>(
+                            operator, value -> 0, Types.INT, maxParallelism, 
1, 0);
+            harness.setStateBackend(
+                    new HashMapStateBackend()
+                            .configure(config, 
Thread.currentThread().getContextClassLoader()));
+            harness.open();
+            harness.setStateTtlProcessingTime(0);
+            backend = (AbstractKeyedStateBackend<Integer>) 
operator.<Integer>getKeyedStateBackend();
+            backend.setCurrentKey(0);
+            MapStateDescriptor<ByteArrayWrapper, byte[]> descriptor =
+                    new MapStateDescriptor<>(
+                            "map",
+                            ByteArrayWrapperSerializer.INSTANCE,
+                            BytePrimitiveArraySerializer.INSTANCE);
+            StateTtlConfig.Builder ttl =
+                    StateTtlConfig.newBuilder(Duration.ofMillis(100))
+                            
.setUpdateType(StateTtlConfig.UpdateType.OnReadAndWrite)
+                            .setStateVisibility(visibility)
+                            .disableCleanupInBackground();
+            if (incremental) {
+                ttl.cleanupIncrementally(1, false);
+            }
+            descriptor.enableTimeToLive(ttl.build());
+            state =
+                    backend.getPartitionedState(
+                            VoidNamespace.INSTANCE, 
VoidNamespaceSerializer.INSTANCE, descriptor);
+            if (metrics) {
+                
assertThat(state.getClass().getSimpleName()).isEqualTo("MetricsTrackingMapState");
+            }
+            handler = new BeamMapStateHandler(config);
+        }
+
+        @Override
+        public void close() throws Exception {
+            harness.close();
+        }
+    }
+}
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/AbstractKeyedStateBackend.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/AbstractKeyedStateBackend.java
index 1bec7b2218d..f34378a250a 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/AbstractKeyedStateBackend.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/AbstractKeyedStateBackend.java
@@ -262,6 +262,13 @@ public abstract class AbstractKeyedStateBackend<K>
                 KeyGroupRangeAssignment.assignToKeyGroup(newKey, 
numberOfKeyGroups));
     }
 
+    @Override
+    public void setCurrentKeyForStateAccess(K newKey) {
+        this.keyContext.setCurrentKey(newKey);
+        this.keyContext.setCurrentKeyGroupIndex(
+                KeyGroupRangeAssignment.assignToKeyGroup(newKey, 
numberOfKeyGroups));
+    }
+
     @Override
     public void setCurrentKeyAndKeyGroup(K newKey, int newKeyGroupIndex) {
         notifyKeySelected(newKey);
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/KeyedStateBackend.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/KeyedStateBackend.java
index e11ca590ab3..807bf83bc95 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/KeyedStateBackend.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/KeyedStateBackend.java
@@ -43,6 +43,15 @@ public interface KeyedStateBackend<K>
      */
     void setCurrentKey(K newKey);
 
+    /**
+     * Restores the key context for a state request rather than selecting a 
key for another input
+     * record. Backends with record-triggered maintenance should override this 
method to avoid
+     * running that maintenance for the request.
+     */
+    default void setCurrentKeyForStateAccess(K newKey) {
+        setCurrentKey(newKey);
+    }
+
     /**
      * @return Current key.
      */
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/internal/InternalMapState.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/internal/InternalMapState.java
index 92ae9c0c4c0..84572179c8a 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/internal/InternalMapState.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/internal/InternalMapState.java
@@ -21,6 +21,7 @@ package org.apache.flink.runtime.state.internal;
 import org.apache.flink.api.common.state.MapState;
 
 import java.util.Map;
+import java.util.Set;
 
 /**
  * The peer to the {@link MapState} in the internal state type hierarchy.
@@ -33,4 +34,25 @@ import java.util.Map;
  * @param <UV> Type of the value in the state
  */
 public interface InternalMapState<K, N, UK, UV>
-        extends InternalKvState<K, N, Map<UK, UV>>, MapState<UK, UV> {}
+        extends InternalKvState<K, N, Map<UK, UV>>, MapState<UK, UV> {
+
+    /**
+     * Returns whether this state has no visible entries other than the 
excluded keys, without
+     * updating TTL timestamps or cleaning up expired entries.
+     *
+     * <p>This allows clients with buffered deletions to check emptiness 
without applying those
+     * deletions or invalidating active iterators. TTL decorators must 
override this method instead
+     * of using their ordinary, potentially mutating key iteration.
+     */
+    default boolean isEmptyExcluding(Set<?> excludedKeys) throws Exception {
+        final Iterable<UK> keys = keys();
+        if (keys != null) {
+            for (UK key : keys) {
+                if (!excludedKeys.contains(key)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+}
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/metrics/MetricsTrackingMapState.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/metrics/MetricsTrackingMapState.java
index 872b12ea691..f693ffdd97e 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/metrics/MetricsTrackingMapState.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/metrics/MetricsTrackingMapState.java
@@ -29,6 +29,7 @@ import org.apache.flink.util.function.ThrowingConsumer;
 import java.io.IOException;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * This class wraps map state with latency tracking logic.
@@ -261,6 +262,22 @@ class MetricsTrackingMapState<K, N, UK, UV>
         }
     }
 
+    @Override
+    public boolean isEmptyExcluding(Set<?> excludedKeys) throws Exception {
+        if (sizeTrackingStateMetric != null && 
sizeTrackingStateMetric.trackMetricsOnIsEmpty()) {
+            sizeTrackingStateMetric.updateMetrics(
+                    MapStateMetrics.MAP_STATE_IS_EMPTY_KEY_SIZE, 
super.sizeOfKey());
+        }
+        if (latencyTrackingStateMetric != null
+                && latencyTrackingStateMetric.trackMetricsOnIsEmpty()) {
+            return trackLatencyWithException(
+                    () -> original.isEmptyExcluding(excludedKeys),
+                    MapStateMetrics.MAP_STATE_IS_EMPTY_LATENCY);
+        } else {
+            return original.isEmptyExcluding(excludedKeys);
+        }
+    }
+
     protected long sizeOfKeyAndUserKey(UK userKey) throws IOException {
 
         if (userKeySerializer == null || userKey == null) {
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/ttl/TtlMapState.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/ttl/TtlMapState.java
index 2dc2df3a031..242fa43c23d 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/state/ttl/TtlMapState.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/state/ttl/TtlMapState.java
@@ -33,6 +33,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Set;
 import java.util.function.Function;
 
 /**
@@ -132,6 +133,24 @@ class TtlMapState<K, N, UK, UV>
         return original.isEmpty();
     }
 
+    @Override
+    public boolean isEmptyExcluding(Set<?> excludedKeys) throws Exception {
+        // Ordinary TTL reads can clean up entries and invalidate another 
active iterator.
+        final Iterable<Map.Entry<UK, TtlValue<UV>>> entries = 
original.entries();
+        final long now = timeProvider.currentTimestamp();
+        if (entries != null) {
+            for (Map.Entry<UK, TtlValue<UV>> entry : entries) {
+                final TtlValue<UV> value = entry.getValue();
+                if (!excludedKeys.contains(entry.getKey())
+                        && value != null
+                        && (returnExpired || !TtlUtils.expired(value, ttl, 
now))) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
     @Nullable
     @Override
     public Map<UK, TtlValue<UV>> getUnexpiredOrNull(@Nonnull Map<UK, 
TtlValue<UV>> ttlValue) {
diff --git 
a/flink-runtime/src/test/java/org/apache/flink/runtime/state/StateBackendTestBase.java
 
b/flink-runtime/src/test/java/org/apache/flink/runtime/state/StateBackendTestBase.java
index 5b2a80a9549..32ab5396c43 100644
--- 
a/flink-runtime/src/test/java/org/apache/flink/runtime/state/StateBackendTestBase.java
+++ 
b/flink-runtime/src/test/java/org/apache/flink/runtime/state/StateBackendTestBase.java
@@ -3651,6 +3651,37 @@ public abstract class StateBackendTestBase<B extends 
AbstractStateBackend> {
         }
     }
 
+    @TestTemplate
+    void testMapStateReadOnlyKeyContext() throws Exception {
+        CheckpointableKeyedStateBackend<Integer> backend =
+                createKeyedBackend(IntSerializer.INSTANCE);
+        try {
+            MapState<Integer, Long> state =
+                    backend.getPartitionedState(
+                            VoidNamespace.INSTANCE,
+                            VoidNamespaceSerializer.INSTANCE,
+                            new MapStateDescriptor<>("map", Integer.class, 
Long.class));
+            backend.setCurrentKey(1);
+            state.put(1, 100L);
+            backend.setCurrentKey(2);
+            state.put(1, 200L);
+
+            int[] selections = {0};
+            backend.registerKeySelectionListener(ignored -> selections[0]++);
+            for (int key : new int[] {1, 3, 2, 1}) {
+                backend.setCurrentKeyForStateAccess(key);
+                assertThat(backend.getCurrentKey()).isEqualTo(key);
+                assertThat(state.get(1)).isEqualTo(key == 3 ? null : key * 
100L);
+            }
+            assertThat(selections[0]).isZero();
+            backend.setCurrentKey(1);
+            assertThat(selections[0]).isEqualTo(1);
+        } finally {
+            IOUtils.closeQuietly(backend);
+            backend.dispose();
+        }
+    }
+
     @TestTemplate
     void testMapStateIsEmpty() throws Exception {
         MapStateDescriptor<Integer, Long> kvId =
diff --git 
a/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/ChangelogKeyedStateBackend.java
 
b/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/ChangelogKeyedStateBackend.java
index 621aefb9f8b..624ba448adb 100644
--- 
a/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/ChangelogKeyedStateBackend.java
+++ 
b/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/ChangelogKeyedStateBackend.java
@@ -294,6 +294,11 @@ public class ChangelogKeyedStateBackend<K>
         keyedStateBackend.setCurrentKey(newKey);
     }
 
+    @Override
+    public void setCurrentKeyForStateAccess(K newKey) {
+        keyedStateBackend.setCurrentKeyForStateAccess(newKey);
+    }
+
     @Override
     public K getCurrentKey() {
         return keyedStateBackend.getCurrentKey();
diff --git 
a/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/restore/ChangelogMigrationRestoreTarget.java
 
b/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/restore/ChangelogMigrationRestoreTarget.java
index ba02e93d221..1c668a9a797 100644
--- 
a/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/restore/ChangelogMigrationRestoreTarget.java
+++ 
b/flink-state-backends/flink-statebackend-changelog/src/main/java/org/apache/flink/state/changelog/restore/ChangelogMigrationRestoreTarget.java
@@ -202,6 +202,11 @@ public class ChangelogMigrationRestoreTarget<K> implements 
ChangelogRestoreTarge
                 keyedStateBackend.setCurrentKey(newKey);
             }
 
+            @Override
+            public void setCurrentKeyForStateAccess(K newKey) {
+                keyedStateBackend.setCurrentKeyForStateAccess(newKey);
+            }
+
             @Override
             public void setCurrentKeyAndKeyGroup(K newKey, int 
newKeyGroupIndex) {
                 keyedStateBackend.setCurrentKeyAndKeyGroup(newKey, 
newKeyGroupIndex);
diff --git 
a/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/sync/ForStSyncKeyedStateBackend.java
 
b/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/sync/ForStSyncKeyedStateBackend.java
index 97ab1dfbc1b..48e4ec447d1 100644
--- 
a/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/sync/ForStSyncKeyedStateBackend.java
+++ 
b/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/sync/ForStSyncKeyedStateBackend.java
@@ -524,6 +524,12 @@ public class ForStSyncKeyedStateBackend<K> extends 
AbstractKeyedStateBackend<K>
         sharedRocksKeyBuilder.setKeyAndKeyGroup(getCurrentKey(), 
getCurrentKeyGroupIndex());
     }
 
+    @Override
+    public void setCurrentKeyForStateAccess(K newKey) {
+        super.setCurrentKeyForStateAccess(newKey);
+        sharedRocksKeyBuilder.setKeyAndKeyGroup(getCurrentKey(), 
getCurrentKeyGroupIndex());
+    }
+
     @Override
     public void setCurrentKeyAndKeyGroup(K newKey, int newKeyGroupIndex) {
         super.setCurrentKeyAndKeyGroup(newKey, newKeyGroupIndex);
diff --git 
a/flink-state-backends/flink-statebackend-rocksdb/src/main/java/org/apache/flink/state/rocksdb/RocksDBKeyedStateBackend.java
 
b/flink-state-backends/flink-statebackend-rocksdb/src/main/java/org/apache/flink/state/rocksdb/RocksDBKeyedStateBackend.java
index c6512f4a356..d9958b7566e 100644
--- 
a/flink-state-backends/flink-statebackend-rocksdb/src/main/java/org/apache/flink/state/rocksdb/RocksDBKeyedStateBackend.java
+++ 
b/flink-state-backends/flink-statebackend-rocksdb/src/main/java/org/apache/flink/state/rocksdb/RocksDBKeyedStateBackend.java
@@ -553,6 +553,12 @@ public class RocksDBKeyedStateBackend<K> extends 
AbstractKeyedStateBackend<K> {
         sharedRocksKeyBuilder.setKeyAndKeyGroup(getCurrentKey(), 
getCurrentKeyGroupIndex());
     }
 
+    @Override
+    public void setCurrentKeyForStateAccess(K newKey) {
+        super.setCurrentKeyForStateAccess(newKey);
+        sharedRocksKeyBuilder.setKeyAndKeyGroup(getCurrentKey(), 
getCurrentKeyGroupIndex());
+    }
+
     @Override
     public void setCurrentKeyAndKeyGroup(K newKey, int newKeyGroupIndex) {
         super.setCurrentKeyAndKeyGroup(newKey, newKeyGroupIndex);

Reply via email to