[
https://issues.apache.org/jira/browse/FLINK-34625?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18128669#comment-18128669
]
YifanWu commented on FLINK-34625:
---------------------------------
I reproduced the same behavior with the unmodified Apache Flink 1.20.5 packages
and Apache Beam 2.48.0, and traced it to the Python read cache.
Environment: CPython 3.11.17, Java 11.0.32.1, macOS arm64, official CLI local
MiniCluster, streaming/process mode, HashMap state backend, parallelism 1. Beam
uses its pure-Python fallback in this environment.
With a 5-second TTL, OnCreateAndWrite and NeverReturnExpired, the minimal
example below writes 100, reads it immediately, waits 6 seconds and reads it
again:
* Default python.state.cache-size: immediate=100, after_ttl=100 (expected None).
* The same job with python.state.cache-size=0: immediate=100, after_ttl=None.
The cause is in SynchronousKvRuntimeState.enable_time_to_live():
{code:python}
self._remote_state_backend._state_handler._state_cache._cache._max_entries = 0
{code}
Beam's StateCache now uses _max_weight to decide whether caching is enabled;
_cache is an OrderedDict. Setting _cache._max_entries does not disable it. In
the failing 1.20.5 worker, _max_weight remains 1000 and is_cache_enabled()
remains True. The same assignment is still present on master:
https://github.com/apache/flink/blob/c1e1d359c73f8ddcfb19984a994ce9e438e14177/flink-python/pyflink/fn_execution/state_impl.py
https://github.com/apache/beam/blob/v2.48.0/sdks/python/apache_beam/runners/worker/statecache.py
Proposed fix: disable the existing StateCache in place by setting _max_weight=0
and invalidate its existing entries. Keeping the same object matters because
CachingMapStateHandler already holds a reference to it. Preserve the existing
TTL-specific write-cache policy.
I propose focused regression tests using the real Beam StateCache, covering
NeverReturnExpired, OnReadAndWrite, a policy that permits caching, and the
shared MapState cache reference. Remote responses can be controlled without
sleeping in the tests. No dependency or public API changes are needed.
FLINK-37205 fixed a related cache-capacity adaptation, but did not change this
TTL cache-disabling statement.
I would like to work on this issue. Could a committer confirm the approach and
assign it to me? I plan to submit one focused master PR with the fix and
necessary regression tests. Please also consider a 1.20 backport after review.
Minimal reproducer (the 6-second sleep is only for this manual reproducer, not
the proposed regression tests):
{code:python}
import argparse
import json
import sys
import time
from pyflink.common import Configuration, Time, Types
from pyflink.datastream import RuntimeExecutionMode, StreamExecutionEnvironment
from pyflink.datastream.functions import KeyedProcessFunction
from pyflink.datastream.state import StateTtlConfig, ValueStateDescriptor
class TtlCacheRepro(KeyedProcessFunction):
def open(self, runtime_context):
ttl = (StateTtlConfig.new_builder(Time.seconds(5))
.update_ttl_on_create_and_write()
.never_return_expired()
.build())
descriptor = ValueStateDescriptor('ttl_value', Types.INT())
descriptor.enable_time_to_live(ttl)
self.state = runtime_context.get_state(descriptor)
def process_element(self, value, ctx):
self.state.update(100)
immediate = self.state.value()
time.sleep(6)
expired = self.state.value()
yield json.dumps({'immediate': immediate, 'after_ttl': expired})
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--cache-size', type=int, default=None)
args = parser.parse_args()
config = Configuration()
config.set_string('python.execution-mode', 'process')
config.set_string('python.executable', sys.executable)
config.set_string('python.client.executable', sys.executable)
config.set_string('state.backend.type', 'hashmap')
if args.cache_size is not None:
config.set_integer('python.state.cache-size', args.cache_size)
env = StreamExecutionEnvironment.get_execution_environment(config)
env.set_runtime_mode(RuntimeExecutionMode.STREAMING)
env.set_parallelism(1)
(env.from_collection([1], type_info=Types.INT())
.key_by(lambda value: 0, key_type=Types.INT())
.process(TtlCacheRepro(), output_type=Types.STRING())
.print())
env.execute('process-state-ttl-cache-repro')
{code}
Run it with the official CLI and the same Python interpreter for client and
worker:
{code:bash}
bin/flink run -t local -pyclientexec /path/to/python3.11 -pyexec
/path/to/python3.11 -py ttl_cache_repro.py
bin/flink run -t local -pyclientexec /path/to/python3.11 -pyexec
/path/to/python3.11 -py ttl_cache_repro.py --cache-size 0
{code}
> TTL doesn't seem to work in pyflink
> ------------------------------------
>
> Key: FLINK-34625
> URL: https://issues.apache.org/jira/browse/FLINK-34625
> Project: Flink
> Issue Type: Bug
> Components: API / Python
> Affects Versions: 1.18.1
> Environment: Image used: flink:1.18.1-scala_2.12-java11
> Reporter: Mark Lidenberg
> Priority: Major
>
> I've made a simple example to test the ttl and couldn't get the expected
> results. I went further and replicated this example in Java and it worked
> just fine. There is an inconsistency in behavior, so there is something wrong
> in pyflink or my pyflink setup.
> Here is a code to reproduce. In the example I create a state with ttl 1
> second and then process events every 1.5 seconds and print current state. I
> expect it to print `None, None, None, ...` (because ttl expires after 1.5
> seconds), but instead it prints `None, "state", "state, ...`. In Java it
> works as expected, prints `Null, Null, ...`
> ```python
> import time
> from pyflink.common import Time, Types
> from pyflink.datastream import KeyedProcessFunction, RuntimeContext,
> StreamExecutionEnvironment
> from pyflink.datastream.state import StateTtlConfig, ValueStateDescriptor
> class Processor(KeyedProcessFunction):
> def open(self, runtime_context: RuntimeContext):
> state_descriptor = ValueStateDescriptor(
> name="my_state",
> value_type_info=Types.STRING(),
> )
> state_descriptor.enable_time_to_live(
> ttl_config=StateTtlConfig.new_builder(Time.seconds(1))
> .set_update_type(StateTtlConfig.UpdateType.OnCreateAndWrite)
>
> .set_state_visibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
> .build()
> )
> self.state = runtime_context.get_state(state_descriptor)
> def process_element(self, value: int, ctx: KeyedProcessFunction.Context):
> # Print current state
> print(self.state.value())
> # expect to print `None` all the time, but prints: `None, 'state',
> 'state', ...` instead
> # Update state
> self.state.update("state")
> # sleep to reset the state
> time.sleep(1.5)
> if __name__ == "__main__":
> # Init environment
> environment =
> StreamExecutionEnvironment.get_execution_environment().set_parallelism(1)
> # Setup pipeline
> (
> environment.from_collection(
> collection=list(range(10)),
> )
> .key_by(lambda value: 0)
> .process(Processor())
> )
> # Execute pipeline
> environment.execute("ttl_test")
> ```
>
> ```java
> import org.apache.flink.api.common.state.StateTtlConfig;
> import org.apache.flink.api.common.state.ValueState;
> import org.apache.flink.api.common.state.ValueStateDescriptor;
> import org.apache.flink.api.common.time.Time;
> import org.apache.flink.configuration.Configuration;
> import org.apache.flink.metrics.Histogram;
> import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
> import org.apache.flink.util.Collector;
> import java.io.IOException;
> import java.time.LocalDateTime;
> public class Processor extends KeyedProcessFunction<Integer, String, String> {
> private transient ValueState<String> state;
> @Override
> public void open(Configuration parameters) {
> var stateTtlConfig = StateTtlConfig
> .newBuilder(Time.seconds(1))
> .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
>
> .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
> .build();
> var stateDescriptor = new ValueStateDescriptor<>("state",
> String.class);
> stateDescriptor.enableTimeToLive(stateTtlConfig);
> state = getRuntimeContext().getState(stateDescriptor);
> }
> @Override
> public void processElement(String event, Context context,
> Collector<String> collector) throws IOException, InterruptedException {
> // print state
> var state = state.value();
> System.out.println(state); # prints `Null, Null, ...`
> // update state
> state.update(LocalDateTime.now().toString());
> // sleep to reset the state
> Thread.sleep(1500);
> }
> }
> ```
--
This message was sent by Atlassian Jira
(v8.20.10#820010)