@@ -45,6 +45,7 @@ def worker_function_options(
4545 "secrets" : [household_api_secret ()],
4646 "timeout" : 180 ,
4747 "scaledown_window" : 300 ,
48+ "enable_memory_snapshot" : True ,
4849 }
4950 if environment == "main" :
5051 options ["min_containers" ] = 3
@@ -53,9 +54,69 @@ def worker_function_options(
5354 return options
5455
5556
56- @app .function (** worker_function_options ())
57- def handle_household_request (payload : dict [str , Any ]) -> dict [str , Any ]:
58- configure_google_credentials ()
59- from policyengine_household_api .api import app as flask_app
57+ @app .cls (** worker_function_options ())
58+ class HouseholdWorker :
59+ """Worker class for handling household API requests.
6060
61- return dispatch_to_flask_app (flask_app , payload )
61+ Uses a Modal class with ``@modal.enter(snap=True)`` so the heavy Flask
62+ app import runs at memory-snapshot creation time. Subsequent container
63+ starts restore from the snapshot in seconds rather than re-running the
64+ full policyengine country-package import chain on every cold start.
65+ """
66+
67+ @modal .enter (snap = True )
68+ def load_flask_app (self ) -> None :
69+ # Importing `policyengine_household_api.api` runs
70+ # `initialize_analytics_db_if_enabled` at module level, which opens a
71+ # Cloud SQL connection in environments where analytics is enabled.
72+ # That connection needs GOOGLE_APPLICATION_CREDENTIALS, set by
73+ # `configure_google_credentials()`. Configure credentials first so the
74+ # snapshot-time import can succeed even before any request method runs.
75+ configure_google_credentials ()
76+
77+ from policyengine_household_api .api import app as flask_app
78+
79+ self .flask_app = flask_app
80+
81+ @modal .enter (snap = False )
82+ def reset_post_snapshot_state (self ) -> None :
83+ # Runs on every container start AFTER snapshot restore. Memory
84+ # snapshots preserve Python object state but not live network
85+ # connections; the SQLAlchemy pool and the Cloud SQL Connector
86+ # captured in the snapshot hold sockets that closed at snapshot
87+ # time. Reset them so the first request opens fresh connections.
88+ #
89+ # Also force-recreate the Google credentials file: Modal preserves
90+ # env vars across snapshot restore, but /tmp is not guaranteed to
91+ # be preserved. Without popping the env var first,
92+ # configure_google_credentials() would short-circuit on the
93+ # surviving GOOGLE_APPLICATION_CREDENTIALS and leave it pointing
94+ # at a missing file, breaking analytics DB reconnects.
95+ # See: https://modal.com/docs/guide/memory-snapshot
96+ os .environ .pop ("GOOGLE_APPLICATION_CREDENTIALS" , None )
97+ configure_google_credentials ()
98+
99+ from policyengine_household_api .data import analytics_setup
100+
101+ if not analytics_setup .is_analytics_enabled ():
102+ return
103+
104+ analytics_setup .cleanup ()
105+
106+ try :
107+ with self .flask_app .app_context ():
108+ analytics_setup .db .engine .dispose ()
109+ except Exception as exc :
110+ import logging
111+
112+ logging .getLogger (__name__ ).warning (
113+ "Failed to dispose analytics DB engine after snapshot "
114+ "restore; subsequent queries may reconnect lazily: %s" ,
115+ exc ,
116+ )
117+
118+ @modal .method ()
119+ def handle_household_request (
120+ self , payload : dict [str , Any ]
121+ ) -> dict [str , Any ]:
122+ return dispatch_to_flask_app (self .flask_app , payload )
0 commit comments