@@ -721,6 +721,127 @@ def f(*, x: int) -> int:
721721 assert calls ["n" ] == 1
722722
723723
724+ class TestHybridCache :
725+ """Test HybridCache L1+L2 behavior with l2_ttl."""
726+
727+ def test_l2_ttl_defaults_to_l1_ttl_times_2 (self ):
728+ """Test that l2_ttl defaults to l1_ttl * 2."""
729+ l1 = InMemCache ()
730+ l2 = InMemCache ()
731+
732+ cache = HybridCache (l1_cache = l1 , l2_cache = l2 , l1_ttl = 60 )
733+ assert cache .l1_ttl == 60
734+ assert cache .l2_ttl == 120
735+
736+ def test_l2_ttl_explicit_value (self ):
737+ """Test that explicit l2_ttl is respected."""
738+ l1 = InMemCache ()
739+ l2 = InMemCache ()
740+
741+ cache = HybridCache (l1_cache = l1 , l2_cache = l2 , l1_ttl = 60 , l2_ttl = 300 )
742+ assert cache .l1_ttl == 60
743+ assert cache .l2_ttl == 300
744+
745+ def test_set_respects_l2_ttl (self ):
746+ """Test that set() uses l2_ttl for L2 cache."""
747+ l1 = InMemCache ()
748+ l2 = InMemCache ()
749+
750+ # Set l1_ttl=1, l2_ttl=10
751+ cache = HybridCache (l1_cache = l1 , l2_cache = l2 , l1_ttl = 1 , l2_ttl = 10 )
752+
753+ cache .set ("key1" , "value1" , ttl = 100 )
754+
755+ # Both should have the value immediately
756+ assert cache .get ("key1" ) == "value1"
757+ assert l1 .get ("key1" ) == "value1"
758+ assert l2 .get ("key1" ) == "value1"
759+
760+ # Wait for L1 to expire (l1_ttl=1)
761+ time .sleep (1.2 )
762+
763+ # L1 should be expired, but L2 should still have it
764+ assert l1 .get ("key1" ) is None
765+ assert l2 .get ("key1" ) == "value1"
766+
767+ # HybridCache should fetch from L2 and repopulate L1
768+ assert cache .get ("key1" ) == "value1"
769+ assert l1 .get ("key1" ) == "value1" # L1 repopulated
770+
771+ def test_set_entry_respects_l2_ttl (self ):
772+ """Test that set_entry() uses l2_ttl for L2 cache."""
773+ from advanced_caching .storage import CacheEntry
774+
775+ l1 = InMemCache ()
776+ l2 = InMemCache ()
777+
778+ cache = HybridCache (l1_cache = l1 , l2_cache = l2 , l1_ttl = 1 , l2_ttl = 10 )
779+
780+ now = time .time ()
781+ entry = CacheEntry (value = "test_value" , fresh_until = now + 100 , created_at = now )
782+
783+ cache .set_entry ("key2" , entry , ttl = 100 )
784+
785+ # Both should have the entry (using get() which checks freshness)
786+ assert cache .get ("key2" ) == "test_value"
787+ assert l1 .get ("key2" ) == "test_value"
788+ assert l2 .get ("key2" ) == "test_value"
789+
790+ # Wait for L1 to expire (l1_ttl=1)
791+ time .sleep (1.2 )
792+
793+ # L1 expired (get() returns None for expired), L2 should still have it
794+ assert l1 .get ("key2" ) is None
795+ assert l2 .get ("key2" ) == "test_value"
796+
797+ # HybridCache should fetch from L2
798+ assert cache .get ("key2" ) == "test_value"
799+
800+ def test_set_if_not_exists_respects_l2_ttl (self ):
801+ """Test that set_if_not_exists() uses l2_ttl for L2 cache."""
802+ l1 = InMemCache ()
803+ l2 = InMemCache ()
804+
805+ cache = HybridCache (l1_cache = l1 , l2_cache = l2 , l1_ttl = 1 , l2_ttl = 10 )
806+
807+ # First set should succeed
808+ assert cache .set_if_not_exists ("key3" , "value3" , ttl = 100 ) is True
809+ assert cache .get ("key3" ) == "value3"
810+
811+ # Second set should fail (key exists)
812+ assert cache .set_if_not_exists ("key3" , "value3_new" , ttl = 100 ) is False
813+ assert cache .get ("key3" ) == "value3"
814+
815+ # Wait for L1 to expire
816+ time .sleep (1.2 )
817+
818+ # L2 should still have it, so set_if_not_exists should fail
819+ assert cache .set_if_not_exists ("key3" , "value3_new" , ttl = 100 ) is False
820+
821+ # Value should still be original from L2
822+ assert cache .get ("key3" ) == "value3"
823+
824+ def test_l2_ttl_with_zero_ttl_in_set (self ):
825+ """Test that l2_ttl is used when ttl=0 is passed to set()."""
826+ l1 = InMemCache ()
827+ l2 = InMemCache ()
828+
829+ cache = HybridCache (l1_cache = l1 , l2_cache = l2 , l1_ttl = 2 , l2_ttl = 5 )
830+
831+ # Set with ttl=0 should use l1_ttl and l2_ttl defaults
832+ cache .set ("key4" , "value4" , ttl = 0 )
833+
834+ assert cache .get ("key4" ) == "value4"
835+
836+ # Wait for L1 to expire
837+ time .sleep (2.2 )
838+
839+ # L1 expired, but L2 should still have it (l2_ttl=5)
840+ assert l1 .get ("key4" ) is None
841+ assert l2 .get ("key4" ) == "value4"
842+ assert cache .get ("key4" ) == "value4"
843+
844+
724845class TestNoCachingWhenZero :
725846 """Ensure ttl/interval_seconds == 0 disables caching/background behavior."""
726847
0 commit comments