Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.

Commit fd88c9a

Browse files
Copilotbashandbone
andcommitted
fix: upstream-sync LocalFile resilient list() + notify feature gate + closure lifetime bug
- Add try_ensure_metadata() helper: warns on error, returns Option<&Metadata> - Change file_type().await? to match with warn+continue on error - Remove old symlink error block, replace with try_ensure_metadata - Use is_none_or for file size limit check (skip if metadata unavailable or too big) - Use try_ensure_metadata with let..else for ordinal fetch - Add dep:notify to source-local-file feature (fixes pre-existing CI failure) - Fix apply_component_changes closure lifetime bug (HRTB E0308) in components.rs by using explicit for loops + Vec instead of flat_map iterator chains Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com>
1 parent e3b224d commit fd88c9a

8 files changed

Lines changed: 80 additions & 57 deletions

File tree

crates/recoco-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ source-gdrive = [
249249
source-local-file = [
250250
"batching",
251251
"dep:async-stream",
252+
"dep:notify",
252253
"dep:recoco-splitters",
253254
"recoco-splitters/pattern-matching",
254255
"recoco-utils/bytes_decode",

crates/recoco-core/src/base/value.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,9 +1809,7 @@ mod tests {
18091809
let struct_part = KeyPart::from(vec![
18101810
KeyPart::from(String::from("world")),
18111811
KeyPart::from(100i64),
1812-
KeyPart::from(vec![
1813-
KeyPart::from(false),
1814-
]),
1812+
KeyPart::from(vec![KeyPart::from(false)]),
18151813
]);
18161814
assert_eq!(struct_part.to_strs(), vec!["world", "100", "false"]);
18171815
}

crates/recoco-core/src/ops/sources/amazon_s3.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ use redis::Client as RedisClient;
1919
use std::sync::Arc;
2020
use urlencoding;
2121

22-
use recoco_splitters::pattern_matcher::PatternMatcher;
2322
use crate::base::field_attrs;
2423
use crate::ops::sdk::*;
24+
use recoco_splitters::pattern_matcher::PatternMatcher;
2525

2626
/// Decode a form-encoded URL string, treating '+' as spaces
2727
fn decode_form_encoded_url(input: &str) -> Result<Arc<str>> {

crates/recoco-core/src/ops/sources/azure_blob.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ use azure_storage_blobs::prelude::*;
1818
use futures::StreamExt;
1919
use std::sync::Arc;
2020

21-
use recoco_splitters::pattern_matcher::PatternMatcher;
2221
use crate::base::field_attrs;
2322
use crate::ops::sdk::*;
2423
use crate::prelude::*;
24+
use recoco_splitters::pattern_matcher::PatternMatcher;
2525

2626
#[derive(Debug, Serialize, Deserialize)]
2727
pub struct Spec {

crates/recoco-core/src/ops/sources/google_drive.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
// Both the upstream CocoIndex code and the Recoco modifications are licensed under the Apache-2.0 License.
1111
// SPDX-License-Identifier: Apache-2.0
1212

13-
use recoco_splitters::pattern_matcher::PatternMatcher;
1413
use chrono::Duration;
1514
use google_drive3::{
1615
DriveHub,
@@ -21,6 +20,7 @@ use http_body_util::BodyExt;
2120
use hyper_rustls::HttpsConnector;
2221
use hyper_util::client::legacy::connect::HttpConnector;
2322
use phf::phf_map;
23+
use recoco_splitters::pattern_matcher::PatternMatcher;
2424

2525
use crate::base::field_attrs;
2626
use crate::ops::sdk::*;

crates/recoco-core/src/ops/sources/local_file.rs

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ use std::path::Path;
1717
use std::{path::PathBuf, sync::Arc};
1818
use tracing::warn;
1919

20-
use recoco_splitters::pattern_matcher::PatternMatcher;
2120
use crate::base::field_attrs;
2221
use crate::{fields_value, ops::sdk::*};
22+
use recoco_splitters::pattern_matcher::PatternMatcher;
2323

2424
#[derive(Debug, Serialize, Deserialize)]
2525
pub struct Spec {
@@ -51,6 +51,24 @@ async fn ensure_metadata<'a>(
5151
Ok(metadata.as_ref().unwrap())
5252
}
5353

54+
async fn try_ensure_metadata<'a>(
55+
path: &Path,
56+
metadata: &'a mut Option<Metadata>,
57+
) -> Option<&'a Metadata> {
58+
if metadata.is_none() {
59+
// Follow symlinks.
60+
match tokio::fs::metadata(path).await {
61+
Ok(m) => {
62+
*metadata = Some(m);
63+
}
64+
Err(e) => {
65+
warn!("Failed to get metadata for {}: {e}", path.display());
66+
}
67+
}
68+
}
69+
metadata.as_ref()
70+
}
71+
5472
#[async_trait]
5573
impl SourceExecutor for Executor {
5674
async fn list(
@@ -78,20 +96,21 @@ impl SourceExecutor for Executor {
7896
let mut metadata: Option<Metadata> = None;
7997

8098
// For symlinks, if the target doesn't exist, log and skip.
81-
let file_type = entry.file_type().await?;
82-
if file_type.is_symlink()
83-
&& let Err(e) = ensure_metadata(&path, &mut metadata).await {
84-
if e.kind() == std::io::ErrorKind::NotFound {
85-
warn!("Skipped broken symlink: {}", path.display());
86-
continue;
87-
}
88-
Err(e)?;
99+
let file_type = match entry.file_type().await {
100+
Ok(ft) => ft,
101+
Err(e) => {
102+
warn!("Failed to get file type for {}: {e}", path.display());
103+
continue;
89104
}
105+
};
90106
let is_dir = if file_type.is_dir() {
91107
true
92108
} else if file_type.is_symlink() {
93109
// Follow symlinks to classify the target.
94-
ensure_metadata(&path, &mut metadata).await?.is_dir()
110+
let Some(m) = try_ensure_metadata(&path, &mut metadata).await else {
111+
continue;
112+
};
113+
m.is_dir()
95114
} else {
96115
false
97116
};
@@ -102,13 +121,17 @@ impl SourceExecutor for Executor {
102121
} else if self.pattern_matcher.is_file_included(relative_path) {
103122
// Check file size limit
104123
if let Some(max_size) = self.max_file_size
105-
&& let Ok(metadata) = ensure_metadata(&path, &mut metadata).await
106-
&& metadata.len() > max_size as u64
124+
&& try_ensure_metadata(&path, &mut metadata)
125+
.await
126+
.is_none_or(|m| m.len() > max_size as u64)
107127
{
108128
continue;
109129
}
110130
let ordinal: Option<Ordinal> = if options.include_ordinal {
111-
let metadata = ensure_metadata(&path, &mut metadata).await?;
131+
let Some(metadata) = try_ensure_metadata(&path, &mut metadata).await
132+
else {
133+
continue;
134+
};
112135
Some(metadata.modified()?.try_into()?)
113136
} else {
114137
None
@@ -256,26 +279,28 @@ impl SourceExecutor for Executor {
256279
let (tx, mut rx) = mpsc::channel::<PathBuf>(100);
257280

258281
let mut watcher = RecommendedWatcher::new(
259-
move |res: notify::Result<notify::Event>| {
260-
match res {
261-
Ok(event) => {
262-
for path in event.paths {
263-
if let Err(err) = tx.try_send(path) {
264-
use tokio::sync::mpsc::error::TrySendError;
265-
match err {
266-
TrySendError::Full(_) => {
267-
warn!("File watcher channel is full; dropping file change event");
268-
}
269-
TrySendError::Closed(_) => {
270-
warn!("File watcher channel is closed; dropping file change event");
271-
}
282+
move |res: notify::Result<notify::Event>| match res {
283+
Ok(event) => {
284+
for path in event.paths {
285+
if let Err(err) = tx.try_send(path) {
286+
use tokio::sync::mpsc::error::TrySendError;
287+
match err {
288+
TrySendError::Full(_) => {
289+
warn!(
290+
"File watcher channel is full; dropping file change event"
291+
);
292+
}
293+
TrySendError::Closed(_) => {
294+
warn!(
295+
"File watcher channel is closed; dropping file change event"
296+
);
272297
}
273298
}
274299
}
275300
}
276-
Err(e) => {
277-
warn!("File watcher error: {}", e);
278-
}
301+
}
302+
Err(e) => {
303+
warn!("File watcher error: {}", e);
279304
}
280305
},
281306
Config::default(),

crates/recoco-core/src/setup/components.rs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -178,25 +178,26 @@ pub async fn apply_component_changes<D: SetupOperator>(
178178
) -> Result<()> {
179179
// First delete components that need to be removed, with bounded concurrency
180180
// to avoid overloading the underlying store or exhausting connection pools.
181-
run_bounded(changes.iter().flat_map(|change| {
182-
change
183-
.keys_to_delete
184-
.iter()
185-
.map(move |key| change.desc.delete(key, context))
186-
}))
187-
.await?;
181+
let mut delete_futs = Vec::new();
182+
for change in changes.iter().copied() {
183+
for key in change.keys_to_delete.iter() {
184+
delete_futs.push(change.desc.delete(key, context));
185+
}
186+
}
187+
run_bounded(delete_futs).await?;
188188

189189
// Then upsert components that need to be updated, also with bounded concurrency.
190-
run_bounded(changes.iter().flat_map(|change| {
191-
change.states_to_upsert.iter().map(move |state| async move {
192-
if state.already_exists {
193-
change.desc.update(&state.state, context).await
190+
let mut upsert_futs = Vec::new();
191+
for change in changes.iter().copied() {
192+
for state in change.states_to_upsert.iter() {
193+
upsert_futs.push(if state.already_exists {
194+
change.desc.update(&state.state, context)
194195
} else {
195-
change.desc.create(&state.state, context).await
196-
}
197-
})
198-
}))
199-
.await?;
196+
change.desc.create(&state.state, context)
197+
});
198+
}
199+
}
200+
run_bounded(upsert_futs).await?;
200201

201202
Ok(())
202203
}

crates/recoco-core/src/setup/db_metadata.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,10 @@ pub async fn read_setup_metadata(pool: &PgPool) -> Result<Option<Vec<SetupMetada
6767
// Use to_regclass to check existence: it respects the connection's search_path
6868
// and schema qualification, so it works correctly regardless of whether a custom
6969
// db_schema_name is configured or the connection uses a non-public default schema.
70-
let exists: Option<bool> = sqlx::query_scalar(
71-
"SELECT to_regclass($1) IS NOT NULL",
72-
)
73-
.bind(&table_name)
74-
.fetch_one(&mut *db_conn)
75-
.await?;
70+
let exists: Option<bool> = sqlx::query_scalar("SELECT to_regclass($1) IS NOT NULL")
71+
.bind(&table_name)
72+
.fetch_one(&mut *db_conn)
73+
.await?;
7674
if !exists.unwrap_or(false) {
7775
None
7876
} else {

0 commit comments

Comments
 (0)