@@ -42,7 +42,7 @@ def _recording_flush(mgr):
4242 in-flight guard contract (the real _flush's finally block)."""
4343 calls = []
4444
45- def _fake (dest , tables , through , trigger ):
45+ def _fake (dest , tables , through , trigger , est_bytes = None ):
4646 calls .append ((dest , tables , through , trigger ))
4747 with mgr ._lock :
4848 mgr ._inflight .discard (dest )
@@ -179,7 +179,7 @@ def test_no_second_flush_while_in_flight_and_swap_keeps_buffering():
179179 started = threading .Event ()
180180 seen = []
181181
182- def slow_flush (dest , tables , through , trigger ):
182+ def slow_flush (dest , tables , through , trigger , est_bytes = None ):
183183 seen .append ((sum (t .num_rows for t in tables ), through ))
184184 started .set ()
185185 release .wait (5 )
@@ -1870,3 +1870,230 @@ def _record(pool, d, b, **kw):
18701870 # Cursor: clamp floor wins (50 > entry cov 10); the entry completing
18711871 # must not regress it, and nothing may persist beyond it early.
18721872 assert mgr .status_snapshot ()["d1" ].flushed_snapshot == 50
1873+
1874+
1875+ # ---------------------------------------------------------------------------
1876+ # Honest-bytes estimation (the 2026-08-26 nbytes-inflation regression class)
1877+ # ---------------------------------------------------------------------------
1878+
1879+
1880+ def _offset_sliced_table (parent_rows : int , offset : int , n : int ) -> pa .Table :
1881+ """An entry-shaped table reproducing the PRODUCTION nbytes inflation:
1882+ a slice of a dictionary-encoded column counts the entire shared
1883+ dictionary (measured ~41KB/row against ~hundreds true), and
1884+ combine_chunks does not prune it. Plain flat-array slices do NOT
1885+ over-report on current pyarrow — the dictionary is the mechanism."""
1886+ dictionary = pa .array (["v" * 200 + str (i ) for i in range (20_000 )])
1887+ indices = pa .array ([i % 10 for i in range (parent_rows )], type = pa .int32 ())
1888+ parent = pa .table (
1889+ {
1890+ "a" : pa .array (range (parent_rows ), type = pa .int64 ()),
1891+ "s" : pa .DictionaryArray .from_arrays (indices , dictionary ),
1892+ }
1893+ )
1894+ return parent .slice (offset , n )
1895+
1896+
1897+ class TestHonestBytes :
1898+ def test_offset_slice_nbytes_overreports (self ):
1899+ # Fixture sanity: the pathology must exist for these tests to
1900+ # mean anything. A 100-row slice of a 100K-row parent reports
1901+ # the parent's buffers.
1902+ sliced = _offset_sliced_table (100_000 , 5_000 , 100 )
1903+ from viaduck .delivery import _estimate_row_bytes
1904+
1905+ honest = _estimate_row_bytes (sliced ) * sliced .num_rows
1906+ assert sliced .nbytes > honest * 50
1907+ # combine_chunks alone does NOT fix it (dictionary retained):
1908+ assert sliced .combine_chunks ().nbytes > honest * 50
1909+
1910+ def test_estimate_row_bytes_matches_honest_copy (self ):
1911+ from viaduck .delivery import _estimate_row_bytes
1912+
1913+ sliced = _offset_sliced_table (100_000 , 5_000 , 2_000 )
1914+ decoded = sliced .combine_chunks ()
1915+ decoded = pa .Table .from_arrays (
1916+ [decoded .column ("a" ), decoded .column ("s" ).cast (pa .string ())],
1917+ names = ["a" , "s" ],
1918+ )
1919+ honest_per_row = decoded .nbytes / sliced .num_rows
1920+ est = _estimate_row_bytes (sliced )
1921+ assert honest_per_row * 0.5 <= est <= honest_per_row * 2
1922+
1923+ def test_estimate_row_bytes_empty_table (self ):
1924+ from viaduck .delivery import _estimate_row_bytes
1925+
1926+ assert _estimate_row_bytes (_offset_sliced_table (10 , 0 , 0 )) == 0
1927+
1928+ def test_split_sizes_from_honest_bytes_not_inflated (self ):
1929+ # Adaptive byte-cut against an offset-sliced entry: the split must
1930+ # come out near target/honest_per_row. The inflated math produced
1931+ # target/parent_bytes-per-row — crumbs ~100x smaller.
1932+ from viaduck .delivery import _estimate_row_bytes
1933+
1934+ mgr , _ , _ = _manager (flush_batch_max_rows = 0 , flush_interval_seconds = 0.0 , flush_adaptive = True )
1935+ entry = _offset_sliced_table (100_000 , 5_000 , 10_000 )
1936+ per_row = _estimate_row_bytes (entry )
1937+ target = per_row * 1_000 # honest target: ~1,000 rows
1938+ with mgr ._lock :
1939+ mgr ._flush_target ["d1" ] = target
1940+ fake , calls = _recording_flush (mgr )
1941+ _ , epoch = mgr .read_plan ()["d1" ]
1942+ mgr .buffer ("d1" , entry , through_snapshot = 5 , epoch = epoch )
1943+ with patch .object (mgr , "_flush" , fake ):
1944+ mgr .maybe_flush ()
1945+ assert calls , "expected a flush"
1946+ rows = calls [0 ][1 ][0 ].num_rows
1947+ assert 500 <= rows <= 1_100 , f"split of { rows } rows is not honest-sized (crumb regression)"
1948+
1949+ def test_flush_receives_honest_est_bytes (self ):
1950+ # The adaptive controller's evidence must be in the same units as
1951+ # the cut. Capture the est_bytes handed to _flush and compare to
1952+ # the honest sample, not the inflated entry nbytes.
1953+ from viaduck .delivery import _estimate_row_bytes
1954+
1955+ mgr , _ , _ = _manager (flush_batch_max_rows = 0 , flush_interval_seconds = 0.0 , flush_adaptive = True )
1956+ entry = _offset_sliced_table (100_000 , 5_000 , 500 )
1957+ per_row = _estimate_row_bytes (entry )
1958+ with mgr ._lock :
1959+ mgr ._flush_target ["d1" ] = per_row * 10_000 # far above the entry: no split
1960+ received = []
1961+
1962+ def _fake (dest , tables , through , trigger , est_bytes = None ):
1963+ received .append (est_bytes )
1964+ with mgr ._lock :
1965+ mgr ._inflight .discard (dest )
1966+
1967+ _ , epoch = mgr .read_plan ()["d1" ]
1968+ mgr .buffer ("d1" , entry , through_snapshot = 5 , epoch = epoch )
1969+ with patch .object (mgr , "_flush" , _fake ):
1970+ mgr .maybe_flush (shutdown = True )
1971+ assert received and received [0 ] is not None
1972+ assert received [0 ] == 500 * per_row
1973+ assert received [0 ] < entry .nbytes / 10 # decisively not the inflated number
1974+
1975+ def test_adaptive_growth_gate_passes_with_honest_fill (self ):
1976+ # Frozen-controller regression: a target-sized split flush must
1977+ # satisfy the fill gate when judged in honest units.
1978+ mgr , _ , _ = _manager (
1979+ flush_batch_max_rows = 0 ,
1980+ flush_interval_seconds = 0.0 ,
1981+ flush_adaptive = True ,
1982+ )
1983+ with mgr ._lock :
1984+ cur = mgr ._flush_target ["d1" ] = 1_000_000
1985+ honest_batch = 900_000 # fill 0.9 >= 0.7 gate
1986+ mgr ._adapt_flush_target ("d1" , duration = 0.01 , batch_bytes = honest_batch )
1987+ with mgr ._lock :
1988+ grown = mgr ._flush_target ["d1" ]
1989+ assert grown > cur , "growth gate did not pass with honest fill"
1990+
1991+
1992+ class TestHonestBytesTotality :
1993+ """Review HIGH-1/HIGH-2: the estimator must never raise (poll-cycle
1994+ escape handler exits the pod) and must price NESTED dictionaries
1995+ (which inflate identically but dodge a top-level-only check)."""
1996+
1997+ def _dict_col (self , parent_rows ):
1998+ dictionary = pa .array (["v" * 200 + str (i ) for i in range (20_000 )])
1999+ indices = pa .array ([i % 10 for i in range (parent_rows )], type = pa .int32 ())
2000+ return pa .DictionaryArray .from_arrays (indices , dictionary )
2001+
2002+ def test_struct_of_dict_priced_honestly (self ):
2003+ from viaduck .delivery import _estimate_row_bytes
2004+
2005+ col = pa .StructArray .from_arrays ([self ._dict_col (50_000 )], names = ["inner" ])
2006+ t = pa .table ({"s" : col }).slice (1_000 , 500 )
2007+ est = _estimate_row_bytes (t )
2008+ assert 0 < est < 2_000 , f"nested dict unpriced or inflated: { est } "
2009+
2010+ def test_list_of_dict_priced_honestly (self ):
2011+ from viaduck .delivery import _estimate_row_bytes
2012+
2013+ inner = self ._dict_col (50_000 )
2014+ offsets = pa .array (range (0 , 50_001 ), type = pa .int32 ())
2015+ col = pa .ListArray .from_arrays (offsets , inner )
2016+ t = pa .table ({"l" : col }).slice (1_000 , 500 )
2017+ est = _estimate_row_bytes (t )
2018+ assert 0 < est < 2_000 , f"list<dict> unpriced or inflated: { est } "
2019+
2020+ def test_dict_of_struct_never_raises (self ):
2021+ # dict<struct> casts are unsupported in pyarrow — the estimator
2022+ # must degrade to 0 (byte-cut inert), never propagate.
2023+ from viaduck .delivery import _estimate_row_bytes
2024+
2025+ struct_vals = pa .StructArray .from_arrays ([pa .array (["x" * 100 ] * 50 )], names = ["f" ])
2026+ indices = pa .array ([i % 50 for i in range (5_000 )], type = pa .int32 ())
2027+ col = pa .DictionaryArray .from_arrays (indices , struct_vals )
2028+ t = pa .table ({"d" : col })
2029+ est = _estimate_row_bytes (t ) # must not raise
2030+ assert est >= 0
2031+
2032+ def test_multichunk_dictionary_entry_priced_honestly (self ):
2033+ from viaduck .delivery import _estimate_row_bytes
2034+
2035+ parent = pa .table ({"s" : self ._dict_col (100_000 )})
2036+ cat = pa .concat_tables ([parent .slice (i * 100 , 100 ) for i in range (50 )])
2037+ assert cat .nbytes > 50_000_000 # per-chunk whole-dictionary inflation
2038+ est = _estimate_row_bytes (cat )
2039+ assert 0 < est < 2_000
2040+
2041+ def test_aimd_grows_after_real_split_flush (self ):
2042+ # End-to-end wiring (review MEDIUM-2): through the REAL _flush,
2043+ # a fast target-sized split flush must grow the target. Reverting
2044+ # the est_bytes plumbing in _flush makes this fail.
2045+ from viaduck .delivery import _estimate_row_bytes
2046+
2047+ mgr , _ , _ = _manager (flush_batch_max_rows = 0 , flush_interval_seconds = 0.0 , flush_adaptive = True )
2048+ entry = _offset_sliced_table (100_000 , 5_000 , 10_000 )
2049+ per_row = _estimate_row_bytes (entry )
2050+ target = per_row * 2_000
2051+ with mgr ._lock :
2052+ mgr ._flush_target ["d1" ] = target
2053+ _ , epoch = mgr .read_plan ()["d1" ]
2054+ mgr .buffer ("d1" , entry , through_snapshot = 5 , epoch = epoch )
2055+ # Mock the destination WRITE only — _flush's own est/adapt logic
2056+ # stays real, which is the wiring under test.
2057+ with patch ("viaduck.delivery.append_only" , return_value = 2_000 ):
2058+ assert mgr .maybe_flush () == 1
2059+ mgr ._executor .shutdown (wait = True )
2060+ with mgr ._lock :
2061+ grown = mgr ._flush_target ["d1" ]
2062+ assert grown > target , f"AIMD did not grow ({ target } -> { grown } ); est_bytes plumbing broken"
2063+
2064+ def test_aimd_no_spurious_growth_on_underfilled_flush (self ):
2065+ # THE discriminator for the _flush est_bytes plumbing (review N2):
2066+ # a 500-row flush against a 2,000-row-equivalent target has honest
2067+ # fill 0.25 < 0.7 -> NO growth. The reverted (inflated-nbytes)
2068+ # wiring reads ~4MB >= 0.7*target and grows spuriously — this test
2069+ # fails under that revert; the growth test alone cannot (inflated
2070+ # >= honest always, so growth happens in both worlds).
2071+ from viaduck .delivery import _estimate_row_bytes
2072+
2073+ mgr , _ , _ = _manager (flush_batch_max_rows = 0 , flush_interval_seconds = 0.0 , flush_adaptive = True )
2074+ entry = _offset_sliced_table (100_000 , 5_000 , 500 )
2075+ per_row = _estimate_row_bytes (entry )
2076+ target = per_row * 2_000
2077+ with mgr ._lock :
2078+ mgr ._flush_target ["d1" ] = target
2079+ _ , epoch = mgr .read_plan ()["d1" ]
2080+ mgr .buffer ("d1" , entry , through_snapshot = 5 , epoch = epoch )
2081+ with patch ("viaduck.delivery.append_only" , return_value = 500 ):
2082+ assert mgr .maybe_flush (shutdown = True ) == 1
2083+ mgr ._executor .shutdown (wait = True )
2084+ with mgr ._lock :
2085+ after = mgr ._flush_target ["d1" ]
2086+ assert after == target , f"spurious growth { target } -> { after } : _flush fed inflated bytes to the fill gate"
2087+
2088+ def test_fixed_size_list_of_dict_priced_honestly (self ):
2089+ # Review N1: fixed_size_list<dict> dodged the rewrite and kept
2090+ # ~19x inflation silently.
2091+ from viaduck .delivery import _estimate_row_bytes
2092+
2093+ dictionary = pa .array (["v" * 200 + str (i ) for i in range (20_000 )])
2094+ indices = pa .array ([i % 10 for i in range (20_000 )], type = pa .int32 ())
2095+ inner = pa .DictionaryArray .from_arrays (indices , dictionary )
2096+ col = pa .FixedSizeListArray .from_arrays (inner , 2 )
2097+ t = pa .table ({"fl" : col }).slice (1_000 , 500 )
2098+ est = _estimate_row_bytes (t )
2099+ assert 0 < est < 3_000 , f"fixed_size_list<dict> unpriced or inflated: { est } "
0 commit comments