JiriOndrusek commented on code in PR #9006:
URL: https://github.com/apache/camel-quarkus/pull/9006#discussion_r3783569813


##########
extensions-support/langchain4j/runtime/src/test/java/org/apache/camel/quarkus/component/support/langchain4j/tracker/jdbc/JdbcIngestionTrackerTest.java:
##########
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.support.langchain4j.tracker.jdbc;
+
+import java.util.List;
+import java.util.UUID;
+
+import 
org.apache.camel.quarkus.component.support.langchain4j.tracker.IngestionTracker;
+import org.h2.jdbcx.JdbcDataSource;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The behavioural contract of the {@link IngestionTracker} SPI, exercised 
against the JDBC
+ * implementation on H2. The test methods are deliberately written against the 
SPI only — nothing
+ * below {@link #setUpTracker()} references {@link JdbcIngestionTracker}.
+ *
+ * <p>
+ * The contract lives folded into this class only because a single 
implementation exists today.
+ * When a second one arrives — e.g. an adapter over a future upstream 
LangChain4j record manager
+ * (langchain4j#2931) — extract the test methods into an abstract {@code 
IngestionTrackerContract}
+ * base class with a {@code createTracker()} factory, and keep one {@code 
*Test} subclass per
+ * implementation: a replacement is a drop-in exactly when its subclass passes.
+ */
+class JdbcIngestionTrackerTest {
+
+    IngestionTracker tracker;
+
+    /** A fresh, empty tracker per test. */
+    @BeforeEach
+    void setUpTracker() {
+        JdbcDataSource dataSource = new JdbcDataSource();
+        dataSource.setURL("jdbc:h2:mem:" + UUID.randomUUID() + 
";DB_CLOSE_DELAY=-1");
+        tracker = new JdbcIngestionTracker(dataSource);
+        tracker.ensureSchema();
+    }
+
+    @Test
+    void unknownDocumentReadsEmpty() {
+        assertTrue(tracker.read("p", "missing").isEmpty());
+    }
+
+    @Test
+    void intentIsDurableAndNeverSkippable() {
+        tracker.writeIntent("p", "doc", "fp1", "hash1", 0, 5, 
IngestionTracker.ORIGIN_SOURCE);
+
+        IngestionTracker.TrackerRow row = tracker.read("p", 
"doc").orElseThrow();
+        assertFalse(row.done(), "an intent row must not count as done");
+        assertEquals(5, row.maxKnownCount(), "the shrink bound must cover the 
intended count");
+    }
+
+    @Test
+    void commitCompletesTheIntent() {
+        tracker.writeIntent("p", "doc", "fp1", "hash1", 0, 5, 
IngestionTracker.ORIGIN_SOURCE);
+        tracker.commit("p", "doc", "fp1", "hash1", 3);
+
+        IngestionTracker.TrackerRow row = tracker.read("p", 
"doc").orElseThrow();
+        assertTrue(row.done());
+        assertEquals("fp1", row.fingerprint());
+        assertEquals("hash1", row.contentHash());
+        assertEquals(3, row.segmentCount());
+    }
+
+    @Test
+    void reintentKeepsTheLargestKnownCount() {

Review Comment:
   Done — added intent→intent, commit-without-intent, 
markFailed-from-in_progress and refreshFingerprint-on-non-done tests.



##########
integration-tests/langchain4j-ingestion-tracker/src/test/java/org/apache/camel/quarkus/component/langchain4j/ingestiontracker/it/Langchain4jIngestionTrackerTest.java:
##########
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.langchain4j.ingestiontracker.it;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+
+/**
+ * Runs the {@code IngestionTracker} behavioural guarantees (mirrored from 
{@code JdbcIngestionTrackerTest})
+ * against a real PostgreSQL server, provisioned by Quarkus Dev Services, 
through
+ * {@link IngestionTrackerResource}.
+ */
+@QuarkusTest
+class Langchain4jIngestionTrackerTest {

Review Comment:
   Done — unique pipeline id per test.



##########
integration-tests/langchain4j-ingestion-tracker/src/main/java/org/apache/camel/quarkus/component/langchain4j/ingestiontracker/it/IngestionTrackerResource.java:
##########
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.langchain4j.ingestiontracker.it;
+
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.QueryParam;
+import 
org.apache.camel.quarkus.component.support.langchain4j.tracker.IngestionTracker;
+import 
org.apache.camel.quarkus.component.support.langchain4j.tracker.IngestionTracker.TrackerRow;
+import 
org.apache.camel.quarkus.component.support.langchain4j.tracker.jdbc.JdbcIngestionTracker;
+
+/**
+ * Exercises {@link JdbcIngestionTracker} against a real datasource. A REST 
resource rather than a
+ * directly injected test field because {@code @QuarkusIntegrationTest} runs 
the application as a
+ * separate process and cannot use {@code @Inject}.
+ */
+@Path("/ingestion-tracker")
+public class IngestionTrackerResource {
+
+    @Inject
+    DataSource dataSource;
+
+    private IngestionTracker tracker;
+
+    @PostConstruct

Review Comment:
   Done — `@ApplicationScoped` with a `StartupEvent` observer.



##########
extensions-support/langchain4j/README.adoc:
##########
@@ -0,0 +1,107 @@
+= LangChain4j Support
+
+This module provides common support code shared by Camel Quarkus LangChain4j 
extensions.
+
+== Ingestion tracker
+
+Status: *Experimental*.
+
+`org.apache.camel.quarkus.component.support.langchain4j.tracker.IngestionTracker`
 is the bookkeeping
+SPI behind keeping a vector store in sync with a changing document source 
(files, buckets, feeds,
+...). Vector stores cannot be enumerated for "what did I already ingest", and 
LangChain4j has no
+equivalent of LangChain-Python's `RecordManager`
+(https://github.com/langchain4j/langchain4j/issues/2931[langchain4j#2931]), so 
without external
+bookkeeping any ingestion pipeline is either append-only (restarts re-embed 
the whole corpus,
+edited documents join their previous vectors instead of replacing them) or 
wipe-and-reload
+(loses everything between passes). `IngestionTracker` closes that gap: it 
tracks one row per document
+— fingerprint, content hash, segment count, a two-phase 
`in_progress`/`done`/`failed` status,
+and `tombstone`/`pinned` flags — as the single authority on what was ingested. 
The vector store
+itself stays a disposable projection that is never asked questions; losing the 
tracker only costs
+re-ingestion, never correctness, because segment ids are deterministic.
+
+The two-phase `writeIntent`/`commit` protocol is what makes the tracker 
crash-safe: a row left
+`in_progress` by a crash (killed process, OOM, ...) is never mistaken for 
"already ingested", so
+the next delivery of the same document converges the store instead of skipping 
it.
+
+=== Document states
+
+Each document is one row that moves through the states below. Transitions are 
the SPI methods;
+the annotations in parentheses describe when the consumer (the ingestion 
pipeline) invokes them.
+
+----
+ (no row)
+    │  writeIntent                 durable BEFORE the store is touched; a 
crash strands
+    ▼                              the row here, and an in_progress row is 
never trusted
+ in_progress                       as "already ingested" — the next delivery 
of the
+    │  commit                      document converges the store
+    ▼
+  done ◄──────────────────────┐
+    │                         │
+    ├─ (unchanged) ──► done   │  no transition: the skip that makes restarts 
free
+    │                         │
+    ├─ refreshFingerprint ────┘  fingerprint renewed after a content-hash match
+    │                            (tier-2), so the cheap tier-1 check works 
next pass
+    │
+    ├─ writeIntent ──► in_progress    (edited content: replace; the shrink 
bound
+    │                                  max(committed, intended) rides along in 
the row)
+    │
+    ├─ markFailed ──► failed          (processing failure — also fired 
straight from
+    │      │                           in_progress; previously committed 
segments, if
+    │      │                           any — a stale but valid version — keep 
serving)
+    │      │
+    │      ├─ (fingerprint unchanged) ──► failed   dead-lettered: skipped on 
every
+    │      │                                       pass until the content 
changes
+    │      │
+    │      └─ writeIntent ──► in_progress          (changed content: retry)
+    │
+    ├─ tombstone() ──► done(0) + tombstone    (explicit delete: the consumer 
removes
+    │      │                                   the vectors and records 
commit(0) with
+    │      │                                   the flag) — "deleted stays 
deleted":
+    │      │                                   every future ingest of the 
document is
+    │      │                                   suppressed, even though the 
source
+    │      │                                   still has it
+    │      │
+    │      └─ unsuppress() ──► done(0)         suppression lifted: the next 
pass
+    │                                          re-ingests the document
+    │
+    └─ pin() ──► done + pinned        (an API write corrected a source-owned 
document)
+           │                           — "the correction wins": source-origin 
updates
+           │                           are suppressed; API-origin writes still 
pass
+           │
+           └─ unpin() ──► done         the source owns the document again
+
+ any state ── deleteRow ──► (no row)   reconciliation: the source no longer 
lists the
+                                       document (guarded by the consumer's pass
+                                       interlock, never fired by the tracker 
itself)
+----
+
+`tombstone` and `pinned` are columns orthogonal to `status`: drawn above off 
`done` because
+that is how the consumer composes them, but they can accompany any state (a 
tombstone can even
+be recorded for a never-ingested document, as a pure suppression record) and 
they survive every

Review Comment:
   Done — the claim is now implemented, and an operator note on `doc_id` 
sensitivity was added.



##########
integration-tests/langchain4j-ingestion-tracker/pom.xml:
##########
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.camel.quarkus</groupId>
+        <artifactId>camel-quarkus-build-parent-it</artifactId>
+        <version>3.39.0-SNAPSHOT</version>
+        <relativePath>../../poms/build-parent-it/pom.xml</relativePath>
+    </parent>
+
+    
<artifactId>camel-quarkus-integration-test-langchain4j-ingestion-tracker</artifactId>
+    <name>Camel Quarkus :: Integration Tests :: LangChain4j Sync Tracker</name>

Review Comment:
   Done — renamed to "Ingestion Tracker".



##########
extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/tracker/jdbc/JdbcIngestionTracker.java:
##########
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.support.langchain4j.tracker.jdbc;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.sql.DataSource;
+
+import 
org.apache.camel.quarkus.component.support.langchain4j.tracker.IngestionTracker;
+
+/**
+ * JDBC-backed {@link IngestionTracker}. Deliberately dialect-free SQL (works 
on PostgreSQL and H2):
+ * upserts are update-then-insert rather than vendor MERGE/ON CONFLICT.
+ *
+ * <p>
+ * <strong>Concurrency assumption: at most one writer per {@code (pipeline, 
documentId)} at a
+ * time.</strong> The update-then-insert is not atomic (each statement 
auto-commits on its own,
+ * there is no transaction), so two concurrent writers for the same row can 
both see the
+ * {@code UPDATE} affect zero rows and then both attempt the {@code INSERT}, 
and one fails on the
+ * {@code (pipeline, doc_id)} primary key. Wrapping the two statements in a 
transaction would not
+ * remove this race without either {@code SERIALIZABLE} isolation or a 
dialect-specific upsert,
+ * both at odds with staying dialect-free; callers (the sync pass runner 
processes one document
+ * at a time per pipeline) are relied upon to hold this invariant instead.
+ *
+ * <p>
+ * <strong>Status: Experimental.</strong> See {@link IngestionTracker}.
+ */
+public class JdbcIngestionTracker implements IngestionTracker {
+
+    static final String TABLE = "cq_ingestion_tracker";

Review Comment:
   Done — renamed to `camel_quarkus_ingestion_tracker`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to