@@ -98,34 +98,20 @@ pub(crate) async fn complete(
9898 let closure = read_accepted_closure_on ( & mut transaction, scope, scope. root_node_id )
9999 . await ?
100100 . ok_or_else ( || GraphError :: Internal ( "accepted closure could not be read" . into ( ) ) ) ?;
101- // The next revision has to clear both sides, not just SQLite. A write
102- // interrupted after the store committed and before SQLite did leaves the
103- // store ahead; allocating from SQLite alone would hand the next closure a
104- // number the store has already used for different content.
105- let recorded = SearchIndexTable :: new ( & mut transaction)
106- . revision ( target)
107- . await ?;
108- let revision = match index_revision ( database, target, recorded) . await {
109- Ok ( revision) => revision,
110- Err ( error) => {
111- transaction. rollback ( ) . await ?;
112- return Err ( error) ;
113- }
114- } ;
115-
116- // Steps 2 and 3.
117- let committed = match index_closure ( database, target, revision, closure) . await {
118- Ok ( committed) => committed,
119- Err ( error) => {
120- transaction. rollback ( ) . await ?;
121- return Err ( error) ;
122- }
123- } ;
124101
125- // Step 4.
126- SearchIndexTable :: new ( & mut transaction)
127- . record_revision ( target, committed)
128- . await ?;
102+ // Steps 2, 3 and 4.
103+ if let Err ( error) = index_and_record (
104+ database,
105+ & mut transaction,
106+ target,
107+ vec ! [ closure] ,
108+ database. expiry ( ) ,
109+ )
110+ . await
111+ {
112+ transaction. rollback ( ) . await ?;
113+ return Err ( error) ;
114+ }
129115 // Step 5.
130116 transaction. commit ( ) . await ?;
131117 // Step 6.
@@ -134,40 +120,66 @@ pub(crate) async fn complete(
134120 . ok_or_else ( || GraphError :: Internal ( "accepted completion could not be read" . into ( ) ) )
135121}
136122
137- /// Write and commit one closure to the search store, under a deadline.
123+ /// Write closures to the search store, commit them, and record the revision in
124+ /// the caller's still-open SQLite transaction — steps 2 to 4 of the ordering.
138125///
139- /// The deadline is required rather than defensive: the SQLite write lock is held
140- /// across this call, so an unbounded search write would stall every other writer
141- /// in the database. On timeout the search transaction is rolled back and the
142- /// write fails, leaving nothing committed anywhere.
143- async fn index_revision (
126+ /// Both accept paths share this. A completion passes one closure; an import
127+ /// passes every closure it materialized, so a whole conversation reaches the
128+ /// store as one transaction carrying one revision.
129+ ///
130+ /// The caller owns the SQLite transaction and must roll it back on error.
131+ /// Nothing here commits it.
132+ pub ( crate ) async fn index_and_record (
144133 database : & GraphDatabase ,
134+ transaction : & mut GraphConnection ,
145135 target : SearchTarget ,
146- recorded : Option < SearchIndexRevision > ,
147- ) -> Result < SearchIndexRevision , GraphError > {
148- let stored = deadline ( database. expiry ( ) , database. search_index . revision ( target) ) . await ?;
149- Ok ( recorded
136+ closures : Vec < AcceptedGraphClosure > ,
137+ expiry : tokio:: time:: Instant ,
138+ ) -> Result < ( ) , GraphError > {
139+ // The next revision has to clear both sides, not just SQLite. A write
140+ // interrupted after the store committed and before SQLite did leaves the
141+ // store ahead; allocating from SQLite alone would hand these closures a
142+ // number the store has already used for different content.
143+ let recorded = SearchIndexTable :: new ( & mut * transaction)
144+ . revision ( target)
145+ . await ?;
146+ let stored = deadline ( expiry, database. search_index . revision ( target) ) . await ?;
147+ let revision = recorded
150148 . max ( stored)
151- . map_or ( SearchIndexRevision :: FIRST , SearchIndexRevision :: next) )
149+ . map_or ( SearchIndexRevision :: FIRST , SearchIndexRevision :: next) ;
150+
151+ let committed = index_closures ( database, target, revision, closures, expiry) . await ?;
152+ SearchIndexTable :: new ( & mut * transaction)
153+ . record_revision ( target, committed)
154+ . await ?;
155+ Ok ( ( ) )
152156}
153157
154- async fn index_closure (
158+ /// Write and commit closures to the search store, under a deadline.
159+ ///
160+ /// The deadline is required rather than defensive: the SQLite write lock is held
161+ /// across this call, so an unbounded search write would stall every other writer
162+ /// in the database. On timeout the search transaction is rolled back and the
163+ /// write fails, leaving nothing committed anywhere.
164+ async fn index_closures (
155165 database : & GraphDatabase ,
156166 target : SearchTarget ,
157167 revision : SearchIndexRevision ,
158- closure : AcceptedGraphClosure ,
168+ closures : Vec < AcceptedGraphClosure > ,
169+ expiry : tokio:: time:: Instant ,
159170) -> Result < SearchIndexRevision , GraphError > {
160171 // One deadline spans the whole sequence rather than each step, because what
161172 // is being bounded is how long the global SQLite write lock is held. A
162173 // per-step budget would let a slow store hold it for a multiple of it.
163- let expiry = database. expiry ( ) ;
164174 let mut write = deadline ( expiry, database. search_index . begin ( target, revision) ) . await ?;
165175 // Past this point a failure has to release the search transaction, or the
166176 // store keeps its write lock and every later write fails behind it. An
167177 // abandoned write releases its own transaction when it is dropped.
168- if let Err ( error) = deadline ( expiry, write. apply ( closure) ) . await {
169- let _ = deadline ( expiry, write. rollback ( ) ) . await ;
170- return Err ( error) ;
178+ for closure in closures {
179+ if let Err ( error) = deadline ( expiry, write. apply ( closure) ) . await {
180+ let _ = deadline ( expiry, write. rollback ( ) ) . await ;
181+ return Err ( error) ;
182+ }
171183 }
172184 // A commit that outlives the deadline is not rolled back, because by then it
173185 // may already have committed. The caller fails the write and rolls SQLite
0 commit comments