@@ -167,6 +167,41 @@ def _get_routing_tag_value(self, table: FeatureView, config: RepoConfig):
167167 tag_name = getattr (config .online_store , "routing_tag" , "tribe" )
168168 return table .tags .get (tag_name )
169169
170+ def _group_by_routing_tag (
171+ self ,
172+ tables : Sequence [FeatureView ],
173+ config : RepoConfig ,
174+ require_tag : bool ,
175+ ) -> Dict [str , List [FeatureView ]]:
176+ """
177+ Bucket FeatureViews by the (lower-cased) value of their routing tag.
178+
179+ Backends act on every FeatureView handed to them, so each one must only
180+ ever see its own bucket. Passing the full list made e.g. SQLite create
181+ tables for Redis-routed views.
182+
183+ Args:
184+ tables: FeatureViews to group.
185+ config: Feast RepoConfig.
186+ require_tag: Raise on an untagged FeatureView instead of skipping it.
187+ Returns:
188+ Mapping of tag value to the FeatureViews carrying it.
189+ Raises:
190+ ValueError: If ``require_tag`` and a FeatureView has no routing tag.
191+ """
192+ grouped : Dict [str , List [FeatureView ]] = {}
193+ for table in tables :
194+ tribe = self ._get_routing_tag_value (table , config )
195+ if not tribe :
196+ if require_tag :
197+ tag_name = getattr (config .online_store , "routing_tag" , "tribe" )
198+ raise ValueError (
199+ f"FeatureView must have a '{ tag_name } ' tag to use HybridOnlineStore."
200+ )
201+ continue
202+ grouped .setdefault (tribe .lower (), []).append (table )
203+ return grouped
204+
170205 def online_write_batch (
171206 self ,
172207 config : RepoConfig ,
@@ -273,31 +308,32 @@ def update(
273308 ValueError: If a FeatureView does not have the required tag.
274309 NotImplementedError: If no online store is found for a tag value.
275310 """
276- for table in tables_to_keep :
277- tribe = self ._get_routing_tag_value (table , config )
278- if not tribe :
279- tag_name = getattr (config .online_store , "routing_tag" , "tribe" )
280- raise ValueError (
281- f"FeatureView must have a '{ tag_name } ' tag to use HybridOnlineStore."
282- )
311+ keep_by_tribe = self ._group_by_routing_tag (
312+ tables_to_keep , config , require_tag = True
313+ )
314+ # Untagged views on the way out are skipped rather than fatal: they may
315+ # predate the routing tag, and there is no backend to route them to.
316+ delete_by_tribe = self ._group_by_routing_tag (
317+ tables_to_delete , config , require_tag = False
318+ )
319+ for tribe in {** keep_by_tribe , ** delete_by_tribe }:
283320 online_store = self ._get_online_store (tribe , config )
284- if online_store :
285- # Local name: rebinding `config` here would feed the next
286- # iteration the selected backend's config instead of the hybrid
287- # one, losing `routing_tag` from the second FeatureView on.
288- store_config = RepoConfig (** self ._prepare_repo_conf (config , tribe ))
289- online_store .update (
290- store_config ,
291- tables_to_delete ,
292- tables_to_keep ,
293- entities_to_delete ,
294- entities_to_keep ,
295- partial ,
296- )
297- else :
321+ if not online_store :
298322 raise NotImplementedError (
299323 f"No online store found for { getattr (config .online_store , 'routing_tag' , 'tribe' )} tag '{ tribe } '. Please check your configuration."
300324 )
325+ # Local name: rebinding `config` here would feed the next iteration
326+ # the selected backend's config instead of the hybrid one, losing
327+ # `routing_tag`.
328+ store_config = RepoConfig (** self ._prepare_repo_conf (config , tribe ))
329+ online_store .update (
330+ store_config ,
331+ delete_by_tribe .get (tribe , []),
332+ keep_by_tribe .get (tribe , []),
333+ entities_to_delete ,
334+ entities_to_keep ,
335+ partial ,
336+ )
301337
302338 def teardown (
303339 self ,
@@ -313,27 +349,13 @@ def teardown(
313349 tables: Sequence of FeatureViews to teardown.
314350 entities: Sequence of Entities to teardown.
315351 """
316- # Use a set of (tribe, store_type, conf_id) to avoid duplicate teardowns for the same instance
317- tribes_seen = set ()
318- online_stores_cfg = getattr (config .online_store , "online_stores" , [])
319- tag_name = getattr (config .online_store , "routing_tag" , "tribe" )
320- for table in tables :
321- tribe = table .tags .get (tag_name )
322- if not tribe :
323- continue
324- # Find all store configs matching this tribe (supporting multiple instances of the same type)
325- for store_cfg in online_stores_cfg :
326- store_type = store_cfg .type
327- # Use id(store_cfg.conf) to distinguish different configs of the same type
328- key = (tribe , store_type , id (store_cfg .conf ))
329- if key in tribes_seen :
330- continue
331- tribes_seen .add (key )
332- # Only select the online store if tribe matches the type (or you can add a mapping in config for more flexibility)
333- if tribe .lower () == store_type .split ("." )[- 1 ].lower ():
334- online_store = self ._get_online_store (tribe , config )
335- if online_store :
336- store_config = RepoConfig (
337- ** self ._prepare_repo_conf (config , tribe )
338- )
339- online_store .teardown (store_config , tables , entities )
352+ # Grouping both dedupes backends and keeps each one from tearing down
353+ # another backend's FeatureViews. Untagged views have no backend to
354+ # route to and are skipped, as before.
355+ for tribe , tribe_tables in self ._group_by_routing_tag (
356+ tables , config , require_tag = False
357+ ).items ():
358+ online_store = self ._get_online_store (tribe , config )
359+ if online_store :
360+ store_config = RepoConfig (** self ._prepare_repo_conf (config , tribe ))
361+ online_store .teardown (store_config , tribe_tables , entities )
0 commit comments