@@ -509,8 +509,15 @@ def _transition_fv_to_materializing(
509509 Transition a feature view to MATERIALIZING state.
510510
511511 Rolls back all already-transitioned FVs if this one can't transition.
512+ Already MATERIALIZING is a no-op (async server may have reserved the state
513+ before returning 202); rollback target is GENERATED in that case.
512514 """
513- previous_states [feature_view .name ] = getattr (feature_view , "state" , None )
515+ current = getattr (feature_view , "state" , None )
516+ if current == FeatureViewState .MATERIALIZING :
517+ previous_states [feature_view .name ] = FeatureViewState .GENERATED
518+ return
519+
520+ previous_states [feature_view .name ] = current
514521 if (
515522 hasattr (feature_view , "state" )
516523 and feature_view .state != FeatureViewState .STATE_UNSPECIFIED
@@ -2501,13 +2508,32 @@ def _delegate_remote_materialize(
25012508 endpoint : str ,
25022509 payload : Dict [str , Any ],
25032510 force : bool = False ,
2511+ run_async : bool = True ,
25042512 ) -> None :
2505- """Fire-and-forget POST to feature server with ?async=true."""
2506- query_params = {"async" : "true" }
2513+ """POST materialize to the feature server.
2514+
2515+ When run_async=True (default), sends ?async=true and returns after 202.
2516+ When run_async=False, omits async and blocks until the server finishes
2517+ synchronous materialization (HTTP response).
2518+ force=True is only valid with run_async=True (server force applies to async).
2519+ """
2520+ if force and not run_async :
2521+ raise ValueError (
2522+ "force=True requires run_async=True. "
2523+ "force only overrides stuck MATERIALIZING on the async path."
2524+ )
2525+
2526+ query_params : Dict [str , str ] = {}
2527+ if run_async :
2528+ query_params ["async" ] = "true"
25072529 if force :
25082530 query_params ["force" ] = "true"
2509- result = self ._post_to_feature_server (endpoint , payload , query_params )
2510- _logger .info ("Remote materialization accepted (%s): %s" , endpoint , result )
2531+
2532+ result = self ._post_to_feature_server (endpoint , payload , query_params or None )
2533+ if run_async :
2534+ _logger .info ("Remote materialization accepted (%s): %s" , endpoint , result )
2535+ else :
2536+ _logger .info ("Remote materialization completed (%s): %s" , endpoint , result )
25112537
25122538 def materialize_incremental (
25132539 self ,
@@ -2516,6 +2542,7 @@ def materialize_incremental(
25162542 full_feature_names : bool = False ,
25172543 version : Optional [str ] = None ,
25182544 force : bool = False ,
2545+ run_async : bool = True ,
25192546 ) -> None :
25202547 """
25212548 Materialize incremental new data from the offline store into the online store.
@@ -2534,8 +2561,11 @@ def materialize_incremental(
25342561 feature view name.
25352562 version (str): Optional version to materialize (e.g., 'v2'). Requires feature_views
25362563 with exactly one entry and enable_online_feature_view_versioning to be enabled.
2537- force (bool): When using remote topology, pass force=true to override stuck
2538- MATERIALIZING state on the feature server. Ignored for local topology.
2564+ force (bool): When using remote topology with run_async=True, pass force=true to
2565+ override stuck MATERIALIZING state on the feature server. Ignored for local topology.
2566+ run_async (bool): When using remote topology, if True (default) POST with ?async=true
2567+ and return after 202. If False, POST without async and block until the server
2568+ finishes sync materialization. Ignored for local topology.
25392569
25402570 Raises:
25412571 Exception: A feature view being materialized does not have a TTL set.
@@ -2560,7 +2590,10 @@ def materialize_incremental(
25602590 if version is not None :
25612591 payload ["version" ] = version
25622592 self ._delegate_remote_materialize (
2563- "/materialize-incremental" , payload , force = force
2593+ "/materialize-incremental" ,
2594+ payload ,
2595+ force = force ,
2596+ run_async = run_async ,
25642597 )
25652598 return
25662599
@@ -2659,7 +2692,9 @@ def tqdm_builder(length):
26592692 else :
26602693 for feature_view , start_date in regular_fvs_with_dates :
26612694 previous_state = getattr (feature_view , "state" , None )
2662- if (
2695+ if previous_state == FeatureViewState .MATERIALIZING :
2696+ previous_state = FeatureViewState .GENERATED
2697+ elif (
26632698 hasattr (feature_view , "state" )
26642699 and feature_view .state != FeatureViewState .STATE_UNSPECIFIED
26652700 ):
@@ -2751,6 +2786,7 @@ def materialize(
27512786 full_feature_names : bool = False ,
27522787 version : Optional [str ] = None ,
27532788 force : bool = False ,
2789+ run_async : bool = True ,
27542790 ) -> None :
27552791 """
27562792 Materialize data from the offline store into the online store.
@@ -2769,8 +2805,11 @@ def materialize(
27692805 feature view name.
27702806 version (str): Optional version to materialize (e.g., 'v2'). Requires feature_views
27712807 with exactly one entry and enable_online_feature_view_versioning to be enabled.
2772- force (bool): When using remote topology, pass force=true to override stuck
2773- MATERIALIZING state on the feature server. Ignored for local topology.
2808+ force (bool): When using remote topology with run_async=True, pass force=true to
2809+ override stuck MATERIALIZING state on the feature server. Ignored for local topology.
2810+ run_async (bool): When using remote topology, if True (default) POST with ?async=true
2811+ and return after 202. If False, POST without async and block until the server
2812+ finishes sync materialization. Ignored for local topology.
27742813
27752814 Examples:
27762815 Materialize all features into the online store over the interval
@@ -2795,7 +2834,9 @@ def materialize(
27952834 }
27962835 if version is not None :
27972836 payload ["version" ] = version
2798- self ._delegate_remote_materialize ("/materialize" , payload , force = force )
2837+ self ._delegate_remote_materialize (
2838+ "/materialize" , payload , force = force , run_async = run_async
2839+ )
27992840 return
28002841
28012842 if utils .make_tzaware (start_date ) > utils .make_tzaware (end_date ):
@@ -2864,7 +2905,9 @@ def tqdm_builder(length):
28642905 else :
28652906 for feature_view , fv_start in regular_fvs_with_dates :
28662907 previous_state = getattr (feature_view , "state" , None )
2867- if (
2908+ if previous_state == FeatureViewState .MATERIALIZING :
2909+ previous_state = FeatureViewState .GENERATED
2910+ elif (
28682911 hasattr (feature_view , "state" )
28692912 and feature_view .state != FeatureViewState .STATE_UNSPECIFIED
28702913 ):
0 commit comments