@@ -69,29 +69,56 @@ impl DynamicFilterTracking {
6969 /// Walk `predicate` once and classify its dynamic-filter content,
7070 /// subscribing to every filter that is not yet complete.
7171 pub fn classify ( predicate : & Arc < dyn PhysicalExpr > ) -> Self {
72+ Self :: classify_with_generation_tag ( predicate) . 0
73+ }
74+
75+ /// Same as [`Self::classify`], but also returns the generation tag of
76+ /// `predicate` at classification time.
77+ ///
78+ /// The tag is the wrapping sum of the generations of all dynamic filters
79+ /// in `predicate` (0 for a [`Self::Static`] predicate). Two callers that
80+ /// classify the same predicate get the same tag if no filter changed
81+ /// between the two calls. For [`Self::Watching`], use
82+ /// [`DynamicFilterTracker::generation_tag`] after
83+ /// [`DynamicFilterTracker::changed`] returns `true` to get the new tag.
84+ /// For the other variants the tag never changes.
85+ pub ( crate ) fn classify_with_generation_tag (
86+ predicate : & Arc < dyn PhysicalExpr > ,
87+ ) -> ( Self , u64 ) {
7288 let mut subscriptions = Vec :: new ( ) ;
89+ let mut completed_generations = 0u64 ;
7390 let mut found_any = false ;
7491 predicate
7592 . apply ( |expr| {
7693 if let Some ( filter) = expr. downcast_ref :: < DynamicFilterPhysicalExpr > ( ) {
7794 found_any = true ;
7895 // Already-complete filters can never change again, so there
79- // is no point subscribing to them.
80- if !filter. is_complete ( ) {
96+ // is no point subscribing to them. Keep their (final)
97+ // generation for the generation tag.
98+ if filter. is_complete ( ) {
99+ completed_generations = completed_generations
100+ . wrapping_add ( filter. current_generation ( ) ) ;
101+ } else {
81102 subscriptions. push ( filter. subscribe ( ) ) ;
82103 }
83104 }
84105 Ok ( TreeNodeRecursion :: Continue )
85106 } )
86107 . expect ( "traversal closure is infallible" ) ;
87108
88- if !found_any {
109+ let tracker = DynamicFilterTracker {
110+ subscriptions,
111+ completed_generations,
112+ } ;
113+ let generation_tag = tracker. generation_tag ( ) ;
114+ let tracking = if !found_any {
89115 DynamicFilterTracking :: Static
90- } else if subscriptions. is_empty ( ) {
116+ } else if tracker . subscriptions . is_empty ( ) {
91117 DynamicFilterTracking :: AllComplete
92118 } else {
93- DynamicFilterTracking :: Watching ( DynamicFilterTracker { subscriptions } )
94- }
119+ DynamicFilterTracking :: Watching ( tracker)
120+ } ;
121+ ( tracking, generation_tag)
95122 }
96123
97124 /// `true` if the predicate contains any dynamic filter (complete or not),
@@ -123,6 +150,10 @@ pub struct DynamicFilterTracker {
123150 /// Subscriptions to the not-yet-complete dynamic filters. Entries are
124151 /// dropped as their filters complete, so the set only shrinks.
125152 subscriptions : Vec < DynamicFilterSubscription > ,
153+ /// Wrapping sum of the final generations of the complete filters: the
154+ /// filters that were complete at classification time and the filters
155+ /// whose subscriptions were dropped. See [`Self::generation_tag`].
156+ completed_generations : u64 ,
126157}
127158
128159impl DynamicFilterTracker {
@@ -134,14 +165,41 @@ impl DynamicFilterTracker {
134165 /// returns `false`.
135166 pub fn changed ( & mut self ) -> bool {
136167 let mut changed = false ;
168+ let completed_generations = & mut self . completed_generations ;
137169 self . subscriptions . retain_mut ( |subscription| {
138170 let change = subscription. observe ( ) ;
139171 changed |= change. changed ;
172+ if change. complete {
173+ // Keep the final generation so the generation tag does not
174+ // change when the subscription is dropped.
175+ * completed_generations =
176+ completed_generations. wrapping_add ( subscription. last_generation ( ) ) ;
177+ }
140178 // Keep the subscription only while the filter can still change.
141179 !change. complete
142180 } ) ;
143181 changed
144182 }
183+
184+ /// A tag that identifies the generations of the watched filters, as
185+ /// observed by the last call to [`Self::changed`] (or at classification
186+ /// time, before the first call).
187+ ///
188+ /// The tag is the wrapping sum of the latest observed generation of every
189+ /// dynamic filter in the predicate, complete or not. It is equal to the
190+ /// tag that [`DynamicFilterTracking::classify_with_generation_tag`]
191+ /// returns for the same predicate, if no filter changed since the last
192+ /// observation. It does not change when a filter completes without an
193+ /// update. A generation only increases, thus in practice two different
194+ /// tags mean that a filter changed. Computing it does not walk the
195+ /// predicate: it only reads the watched subscriptions.
196+ pub ( crate ) fn generation_tag ( & self ) -> u64 {
197+ self . subscriptions
198+ . iter ( )
199+ . fold ( self . completed_generations , |tag, subscription| {
200+ tag. wrapping_add ( subscription. last_generation ( ) )
201+ } )
202+ }
145203}
146204
147205#[ cfg( test) ]
@@ -157,7 +215,7 @@ impl DynamicFilterTracker {
157215 }
158216
159217 /// `true` once every watched filter has completed and been dropped.
160- fn is_exhausted ( & self ) -> bool {
218+ pub ( crate ) fn is_exhausted ( & self ) -> bool {
161219 self . subscriptions . is_empty ( )
162220 }
163221}
@@ -328,4 +386,74 @@ mod tests {
328386 assert ! ( !tracker. changed( ) ) ;
329387 assert ! ( tracker. is_exhausted( ) ) ;
330388 }
389+
390+ #[ test]
391+ fn generation_tag_of_static_predicate_is_zero ( ) {
392+ let ( tracking, tag) =
393+ DynamicFilterTracking :: classify_with_generation_tag ( & lit ( true ) ) ;
394+ assert ! ( matches!( tracking, DynamicFilterTracking :: Static ) ) ;
395+ assert_eq ! ( tag, 0 ) ;
396+ }
397+
398+ #[ test]
399+ fn generation_tag_follows_observed_updates ( ) {
400+ let ( predicate, filter) = dynamic_predicate ( ) ;
401+ let ( mut tracking, initial_tag) =
402+ DynamicFilterTracking :: classify_with_generation_tag ( & predicate) ;
403+ let tracker = tracking. watcher ( ) . unwrap ( ) ;
404+ assert_eq ! ( tracker. generation_tag( ) , initial_tag) ;
405+
406+ // The tag changes only when the tracker observes the update.
407+ filter. update ( lit ( false ) ) . unwrap ( ) ;
408+ assert_eq ! ( tracker. generation_tag( ) , initial_tag) ;
409+ assert ! ( tracker. changed( ) ) ;
410+ let updated_tag = tracker. generation_tag ( ) ;
411+ assert_ne ! ( updated_tag, initial_tag) ;
412+
413+ // A new classification of the same predicate gets the same tag.
414+ let ( _, tag) = DynamicFilterTracking :: classify_with_generation_tag ( & predicate) ;
415+ assert_eq ! ( tag, updated_tag) ;
416+ }
417+
418+ #[ test]
419+ fn generation_tag_is_stable_when_filter_completes ( ) {
420+ let ( predicate, filter) = dynamic_predicate ( ) ;
421+ let ( mut tracking, _) =
422+ DynamicFilterTracking :: classify_with_generation_tag ( & predicate) ;
423+ let tracker = tracking. watcher ( ) . unwrap ( ) ;
424+
425+ filter. update ( lit ( false ) ) . unwrap ( ) ;
426+ assert ! ( tracker. changed( ) ) ;
427+ let tag = tracker. generation_tag ( ) ;
428+
429+ // Completion drops the subscription but keeps its final generation.
430+ filter. mark_complete ( ) ;
431+ assert ! ( !tracker. changed( ) ) ;
432+ assert ! ( tracker. is_exhausted( ) ) ;
433+ assert_eq ! ( tracker. generation_tag( ) , tag) ;
434+
435+ // A predicate classified after completion gets the same tag.
436+ let ( tracking, complete_tag) =
437+ DynamicFilterTracking :: classify_with_generation_tag ( & predicate) ;
438+ assert ! ( matches!( tracking, DynamicFilterTracking :: AllComplete ) ) ;
439+ assert_eq ! ( complete_tag, tag) ;
440+ }
441+
442+ #[ test]
443+ fn generation_tag_of_coalesced_update_and_complete ( ) {
444+ let ( predicate, filter) = dynamic_predicate ( ) ;
445+ let ( mut tracking, initial_tag) =
446+ DynamicFilterTracking :: classify_with_generation_tag ( & predicate) ;
447+ let tracker = tracking. watcher ( ) . unwrap ( ) ;
448+
449+ filter. update ( lit ( false ) ) . unwrap ( ) ;
450+ filter. mark_complete ( ) ;
451+ assert ! ( tracker. changed( ) ) ;
452+ assert ! ( tracker. is_exhausted( ) ) ;
453+ assert_ne ! ( tracker. generation_tag( ) , initial_tag) ;
454+
455+ let ( _, complete_tag) =
456+ DynamicFilterTracking :: classify_with_generation_tag ( & predicate) ;
457+ assert_eq ! ( tracker. generation_tag( ) , complete_tag) ;
458+ }
331459}
0 commit comments