@@ -359,6 +359,113 @@ def fake_stream(func, **kw):
359359 self .assertEqual (len (cached ), 1 )
360360 self .assertIs (cached [0 ], pod )
361361
362+ def test_resource_version_stored_from_watch (self ):
363+ """After the watch stream ends the latest RV is preserved for reconnect."""
364+ pod = _make_pod ("default" , "rv-pod" )
365+ events = [{"type" : "ADDED" , "object" : pod }]
366+
367+ list_func = MagicMock ()
368+ list_resp = MagicMock ()
369+ list_resp .items = []
370+ list_resp .metadata = MagicMock (resource_version = "10" )
371+ list_func .return_value = list_resp
372+
373+ informer = SharedInformer (list_func = list_func )
374+
375+ call_count = {"n" : 0 }
376+
377+ with patch ("kubernetes.informer.informer.Watch" ) as MockWatch :
378+ mock_w = MagicMock ()
379+ mock_w .resource_version = "99"
380+
381+ def fake_stream (func , ** kw ):
382+ call_count ["n" ] += 1
383+ yield from events
384+ informer ._stop_event .set ()
385+
386+ mock_w .stream .side_effect = fake_stream
387+ MockWatch .return_value = mock_w
388+
389+ informer .start ()
390+ informer ._thread .join (timeout = 3 )
391+
392+ # The Watch reported RV "99"; the informer should have stored it.
393+ self .assertEqual (informer ._resource_version , "99" )
394+ # list_func should have been called once for the initial list only.
395+ self .assertEqual (list_func .call_count , 1 )
396+
397+ def test_reconnect_skips_relist_when_rv_known (self ):
398+ """On reconnect without 410 the informer must NOT call the list function again."""
399+ pod = _make_pod ("default" , "reconnect-pod" )
400+
401+ list_func = MagicMock ()
402+ list_resp = MagicMock ()
403+ list_resp .items = [pod ]
404+ list_resp .metadata = MagicMock (resource_version = "5" )
405+ list_func .return_value = list_resp
406+
407+ informer = SharedInformer (list_func = list_func )
408+
409+ stream_calls = {"n" : 0 }
410+
411+ with patch ("kubernetes.informer.informer.Watch" ) as MockWatch :
412+ mock_w = MagicMock ()
413+ mock_w .resource_version = "7"
414+
415+ def fake_stream (func , ** kw ):
416+ stream_calls ["n" ] += 1
417+ if stream_calls ["n" ] == 1 :
418+ # First stream: yield nothing then let it reconnect
419+ return iter ([])
420+ # Second stream: stop the informer
421+ informer ._stop_event .set ()
422+ return iter ([])
423+
424+ mock_w .stream .side_effect = fake_stream
425+ MockWatch .return_value = mock_w
426+
427+ informer .start ()
428+ informer ._thread .join (timeout = 3 )
429+
430+ # list_func is called only once (initial list); reconnect reuses the RV.
431+ self .assertEqual (list_func .call_count , 1 )
432+ self .assertEqual (stream_calls ["n" ], 2 )
433+
434+ def test_410_gone_triggers_relist (self ):
435+ """A 410 Gone ApiException must reset resource_version and trigger re-list."""
436+ from kubernetes .client .exceptions import ApiException
437+
438+ list_func = MagicMock ()
439+ list_resp = MagicMock ()
440+ list_resp .items = []
441+ list_resp .metadata = MagicMock (resource_version = "3" )
442+ list_func .return_value = list_resp
443+
444+ informer = SharedInformer (list_func = list_func )
445+
446+ stream_calls = {"n" : 0 }
447+
448+ with patch ("kubernetes.informer.informer.Watch" ) as MockWatch :
449+ mock_w = MagicMock ()
450+ mock_w .resource_version = "3"
451+
452+ def fake_stream (func , ** kw ):
453+ stream_calls ["n" ] += 1
454+ if stream_calls ["n" ] == 1 :
455+ raise ApiException (status = 410 , reason = "Gone" )
456+ # Second stream (after re-list): stop cleanly
457+ informer ._stop_event .set ()
458+ return iter ([])
459+
460+ mock_w .stream .side_effect = fake_stream
461+ MockWatch .return_value = mock_w
462+
463+ informer .start ()
464+ informer ._thread .join (timeout = 3 )
465+
466+ # list_func called twice: initial list + re-list after 410.
467+ self .assertEqual (list_func .call_count , 2 )
468+
362469
363470if __name__ == "__main__" :
364471 unittest .main ()
0 commit comments