Skip to content

[fix](cloud) bind the packed slice location lifetime to its writer - #67347

Open
liaoxin01 wants to merge 3 commits into
apache:masterfrom
liaoxin01:fix-packed-slice-index-retention
Open

[fix](cloud) bind the packed slice location lifetime to its writer#67347
liaoxin01 wants to merge 3 commits into
apache:masterfrom
liaoxin01:fix-packed-slice-index-retention

Conversation

@liaoxin01

@liaoxin01 liaoxin01 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Problem Summary:

A small file is handed to PackedFileManager when the segment is flushed, but its slice
location is read again only when the rowset is closed — wait_upload_done() from
finish_close(), and get_packed_slice_location() when the rowset meta is built. The gap
between the two is the duration of the whole load, not the lifetime of the packed file.

Both reads went through _global_slice_locations, which cleanup_expired_data() recycles
purely on the age of the small file (uploaded_file_retention_seconds, 1800s by default).
Any load running longer than that lost the mapping and failed with:

[INTERNAL_ERROR]File not found in global index: data/<tablet_id>/<segment>_0.idx

The packed file itself had been uploaded successfully — only the in-memory mapping was gone.
We hit this in production with a broker load that ran for 38 minutes. Two related points:
_uploaded_packed_files is recycled on the same config, so a waiter could also fail with
Packed file not found for path: ...; and this is not specific to inverted index files,
since segments smaller than small_file_threshold_bytes take the same async-close /
late-wait path.

What is changed and how it works?

A slice location should live as long as whoever may still read it, not until a wall clock
deadline.

  1. append_small_file() returns the location as a std::shared_ptr and PackedFileWriter
    keeps it for as long as it lives. wait_upload_done() and the writer's
    get_packed_slice_location() take that reference, and CloudRowsetWriter asks the writer
    instead of the manager, so neither can fail because the index was recycled. The terminal
    upload state (UPLOADED / FAILED) is recorded on the slices, so a waiter does not need the
    PackedFileContext either.

  2. The index itself gets the same property. PackedFileSystem::open_file_impl() falls back
    to _global_slice_locations for a file whose location has not reached the rowset meta
    yet, which is every read during the load — SegmentIndexFileCacheLoader does exactly that
    when it preloads a segment's index and footer ranges into file cache right after close.
    Since a location is now a shared_ptr, the cleanup can tell whether an entry is still
    needed: entries some writer or packed file context still holds are skipped, and the age
    check applies only to those the index alone holds. The last owner going away drops the
    reference, so an entry outlives everything that could still read it.

  3. PackedFileContext::slice_locations is keyed by the small file path, so one path written
    twice into the same packed file left only the second handle in there and the first never
    reached a terminal state. Every appended handle is now kept alongside that map and
    notified from it. No caller does this today —
    CloudTablet::create_transient_rowset_writer() sets allow_packed_file = false, so the
    MOW publish retry that writes one segment path twice never produces packed files — but
    the interface allowed it and failed quietly.

Behavior note: an index entry is no longer guaranteed to be dropped after
uploaded_file_retention_seconds. It now stays for as long as a writer or a packed file
context holds the slice, so a load producing N packed small files keeps N entries resident
until it finishes. That is the point of the fix — those entries were being dropped while
still in use — but it does change how much the map holds during a long load.

Behavior for short loads is unchanged.

Release note

Fix File not found in global index failures for loads that run longer than
uploaded_file_retention_seconds when packed file is enabled.

Check List (For Author)

  • Test

    • Unit Test
  • Behavior changed:

    • No.
  • Does this need documentation?

    • No.

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@liaoxin01
liaoxin01 force-pushed the fix-packed-slice-index-retention branch from ae1340a to 7545454 Compare September 3, 2026 08:41
@liaoxin01 liaoxin01 changed the title [fix](packed-file) keep the packed slice location alive while its writer is alive [fix](packed-file) bind the packed slice location lifetime to its writer Sep 3, 2026
@liaoxin01
liaoxin01 force-pushed the fix-packed-slice-index-retention branch from 7545454 to 9ab63d9 Compare September 3, 2026 08:57
@liaoxin01 liaoxin01 changed the title [fix](packed-file) bind the packed slice location lifetime to its writer [fix](cloud) bind the packed slice location lifetime to its writer Sep 3, 2026
@liaoxin01
liaoxin01 force-pushed the fix-packed-slice-index-retention branch from 9ab63d9 to 668245e Compare September 3, 2026 11:41
A small file is handed to `PackedFileManager` when the segment is flushed
(`close(true)` / `InvertedIndexFileWriter::begin_close()`), but its slice
location is read again much later, when the rowset is finally closed:
`wait_upload_done()` from `finish_close()`, and `get_packed_slice_location()`
when the rowset meta is built. The gap between the two is the duration of the
whole load, not the lifetime of the packed file.

Both reads went through `_global_slice_locations`, which the background cleanup
recycles purely on the age of the small file (`uploaded_file_retention_seconds`,
1800s by default). Any load running longer than that lost the mapping and failed
with:

  [INTERNAL_ERROR]File not found in global index: data/<tablet_id>/<segment>_0.idx

The packed file itself had been uploaded successfully, only the in-memory
mapping was gone. In a case we hit in production a broker load ran for 38
minutes; the index file was packed and uploaded in the first minute and the
rowset was closed in the 38th. `_uploaded_packed_files` is recycled on the same
config, so a load could also fail later with "Packed file not found for path".

Hand the slice location out as a `std::shared_ptr` instead of looking it up by
path:

- `append_small_file()` returns the location, and `PackedFileWriter` keeps it
  for as long as it lives. `wait_upload_done()` and the writer's
  `get_packed_slice_location()` take that reference, so neither can fail because
  the index was recycled, and `CloudRowsetWriter` collects the location from the
  writer rather than from the manager.
- The background cleanup is unchanged: recycling an index entry can no longer
  invalidate a writer, it only drops the by-path lookup that readers use.
- The terminal upload state of a packed file is recorded on the slices it
  contains, so a waiter no longer needs the `PackedFileContext` either. The
  contexts keep their existing retention.

This also removes a hazard around a path being written twice within one process
(a MOW partial update creates a transient rowset writer per publish attempt,
each with the same rowset id and the same first segment id, hence the same
segment path): the two writes now own separate locations, where before the
second one replaced the first in the index and both writers read whichever was
last written.

A null handle means append_small_file() skipped the data without writing it
anywhere, which appendv() normally prevents by switching to direct write first.
`small_file_threshold_bytes` is mutable at runtime though, so lowering it between
the last append and close reaches that path. Report it instead of closing
successfully on a file that does not exist.
@liaoxin01
liaoxin01 force-pushed the fix-packed-slice-index-retention branch from 668245e to f882b77 Compare September 8, 2026 04:11
@liaoxin01

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 100.00% (19/19) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 63.37% (29869/47138)
Line Coverage 48.41% (313594/647796)
Region Coverage 43.90% (252861/575991)
Branch Coverage 45.54% (117938/258985)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (19/19) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 76.45% (34906/45661)
Line Coverage 61.53% (394056/640421)
Region Coverage 57.64% (330713/573788)
Branch Coverage 58.50% (151062/258244)

… held

`cleanup_expired_data()` recycles `_global_slice_locations` purely on the age of
the small file. Age says nothing about whether the entry is still needed: a load
can run for much longer than `uploaded_file_retention_seconds`, and everything it
wrote stays readable through the index until the rowset is committed.

`PackedFileSystem::open_file_impl()` falls back to that index for a file with no
slice location in the rowset meta yet, which is every read that happens during
the load. `SegmentIndexFileCacheLoader` does exactly that: it reads a segment back
right after close, to preload its index and footer ranges into file cache. It
works today only because that read follows the close within seconds.

The previous commit made the slice location a `shared_ptr` held by the writer, so
the index can now tell whether anyone still needs an entry: skip the ones some
writer or packed file context still holds, and let the age check apply only to
those the index alone holds. The last owner going away drops the reference for
us, so the entry's lifetime follows its readers instead of the wall clock.

`use_count()` is only a snapshot, but every transition here is a decrement -- a
handle gains owners in `append_small_file()` before it is ever visible to the
cleanup. Losing a race just keeps an entry for one more round.

`WaitUploadDoneSurvivesRecycledIndexAndContext` asserted the old semantics: it
kept a handle and a context alive across the cleanup and then required the index
to be empty, which is exactly what this change stops from happening. Split it in
two -- one case for a recycled context with the writer still open, one that
removes the index entry outright to show the handle stands on its own.
`PackedFileContext::slice_locations` is keyed by the small file path, so writing
one path twice into the same packed file leaves only the second handle in there.
`mark_slices_upload_result()` iterated that map, so the shadowed slice never
reached a terminal state: it stayed PENDING with `packed_file_size` at -1, and
once the context was recycled its writer's `wait_upload_done()` failed with
`Packed file not found`.

Keep every appended handle alongside the by-path map and notify from that. The
map still describes what goes to the meta service, where a duplicate path cannot
be represented anyway.

No caller does this today -- `CloudTablet::create_transient_rowset_writer()` sets
`allow_packed_file = false`, so the MOW publish retry that writes one segment
path twice never produces packed files. The existing rewrite test rotates the
packed file between the two appends, so it never covered a duplicate within one
packed file either.
@liaoxin01
liaoxin01 force-pushed the fix-packed-slice-index-retention branch from d43901d to a9927c3 Compare September 8, 2026 15:23
@liaoxin01

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 16976 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit a9927c31d8293fcf5195f7bc56cf8306b9012272, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17562	3131	3097	3097
q2	2065	262	226	226
q3	10265	924	515	515
q4	4672	255	203	203
q5	7681	573	388	388
q6	138	115	95	95
q7	541	519	390	390
q8	9246	941	942	941
q9	3522	2436	2441	2436
q10	6501	843	708	708
q11	397	200	185	185
q12	617	261	207	207
q13	18121	1544	1163	1163
q14	165	149	139	139
q15	q16	433	395	372	372
q17	1293	850	822	822
q18	3138	2315	2241	2241
q19	1271	935	773	773
q20	363	292	193	193
q21	5628	1643	1865	1643
q22	333	273	239	239
Total cold run time: 93952 ms
Total hot run time: 16976 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3473	3415	3365	3365
q2	504	409	392	392
q3	2276	2343	2206	2206
q4	1200	1181	906	906
q5	2202	2177	2132	2132
q6	176	121	88	88
q7	1030	943	882	882
q8	1608	1411	1434	1411
q9	3143	3129	3115	3115
q10	1884	1804	1683	1683
q11	364	278	252	252
q12	461	436	342	342
q13	1492	1536	1164	1164
q14	182	170	171	170
q15	q16	399	402	361	361
q17	3681	3400	3279	3279
q18	4833	4475	4825	4475
q19	974	871	862	862
q20	984	948	828	828
q21	3923	3348	3282	3282
q22	395	360	333	333
Total cold run time: 35184 ms
Total hot run time: 31528 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 82340 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit a9927c31d8293fcf5195f7bc56cf8306b9012272, data reload: false

query5	4233	428	342	342
query6	381	139	134	134
query7	4920	423	232	232
query8	295	128	127	127
query9	8707	2964	2984	2964
query10	396	224	175	175
query11	5386	1038	917	917
query12	120	70	71	70
query13	1217	451	325	325
query14	6095	2250	2139	2139
query14_1	1997	1976	1962	1962
query15	169	121	110	110
query16	907	368	340	340
query17	770	439	345	345
query18	2323	323	228	228
query19	155	135	107	107
query20	70	68	70	68
query21	202	101	88	88
query22	5341	5307	5535	5307
query23	6789	6076	6021	6021
query23_1	6043	6149	6113	6113
query24	7267	1072	748	748
query24_1	765	727	763	727
query25	414	290	263	263
query26	1234	235	129	129
query27	2792	418	249	249
query28	4683	1483	1505	1483
query29	917	426	332	332
query30	249	157	129	129
query31	829	402	325	325
query32	130	75	72	72
query33	463	214	170	170
query34	997	820	477	477
query35	415	411	335	335
query36	573	592	547	547
query37	119	87	71	71
query38	995	853	827	827
query39	488	492	470	470
query39_1	474	472	494	472
query40	203	92	78	78
query41	59	55	53	53
query42	77	71	72	71
query43	246	248	215	215
query44	1030	552	545	545
query45	108	100	107	100
query46	786	872	516	516
query47	774	754	740	740
query48	315	315	227	227
query49	533	244	191	191
query50	717	273	194	194
query51	8042	7957	8072	7957
query52	78	73	71	71
query53	191	221	150	150
query54	252	188	147	147
query55	79	68	62	62
query56	209	190	195	190
query57	705	789	657	657
query58	223	175	179	175
query59	1237	1231	1121	1121
query60	258	187	180	180
query61	148	138	131	131
query62	370	222	189	189
query63	179	143	153	143
query64	2912	675	576	576
query65	1667	1677	1696	1677
query66	1830	271	210	210
query67	9715	9753	10082	9753
query68	3014	1212	720	720
query69	343	247	200	200
query70	683	626	614	614
query71	249	179	164	164
query72	2432	1764	1533	1533
query73	667	567	317	317
query74	2002	1236	1162	1162
query75	1178	1108	980	980
query76	2380	740	537	537
query77	261	270	204	204
query78	3971	3578	3224	3224
query79	2862	783	536	536
query80	1605	329	287	287
query81	514	157	132	132
query82	618	131	96	96
query83	281	207	196	196
query84	302	108	86	86
query85	800	359	287	287
query86	478	191	170	170
query87	1025	988	902	902
query88	3081	2116	2131	2116
query89	284	197	180	180
query90	2221	137	124	124
query91	131	118	95	95
query92	97	70	70	70
query93	2430	1041	751	751
query94	654	241	224	224
query95	528	251	296	251
query96	795	601	280	280
query97	1066	1091	1012	1012
query98	179	137	133	133
query99	421	355	322	322
Total cold run time: 180230 ms
Total hot run time: 82340 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 14.76 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit a9927c31d8293fcf5195f7bc56cf8306b9012272, data reload: false

query1	0.00	0.00	0.01
query2	0.07	0.03	0.04
query3	0.25	0.10	0.11
query4	1.60	0.10	0.10
query5	0.18	0.17	0.17
query6	1.24	0.70	0.68
query7	0.03	0.00	0.00
query8	0.05	0.04	0.03
query9	0.28	0.22	0.22
query10	0.35	0.37	0.36
query11	0.17	0.12	0.12
query12	0.15	0.13	0.12
query13	0.31	0.31	0.31
query14	0.46	0.44	0.46
query15	0.37	0.36	0.35
query16	0.23	0.25	0.22
query17	0.75	0.75	0.71
query18	0.19	0.17	0.16
query19	1.20	1.11	1.22
query20	0.01	0.01	0.01
query21	15.44	0.16	0.11
query22	5.05	0.04	0.04
query23	16.17	0.25	0.11
query24	3.00	0.34	0.24
query25	0.10	0.04	0.04
query26	0.76	0.19	0.12
query27	0.03	0.02	0.03
query28	3.62	0.58	0.26
query29	12.46	3.21	2.58
query30	0.27	0.13	0.11
query31	2.76	0.38	0.17
query32	3.53	0.33	0.23
query33	1.52	1.39	1.44
query34	15.45	2.27	1.83
query35	1.81	1.76	1.80
query36	0.47	0.31	0.29
query37	0.06	0.03	0.04
query38	0.05	0.04	0.03
query39	0.03	0.02	0.03
query40	0.12	0.07	0.09
query41	0.07	0.02	0.03
query42	0.04	0.03	0.03
query43	0.03	0.02	0.03
Total cold run time: 90.73 s
Total hot run time: 14.76 s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants