1919)
2020from flask .ctx import has_app_context
2121from flask_migrate import Migrate
22- from flask_socketio import SocketIO
2322from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
2423from sqlalchemy import event
2524from werkzeug .exceptions import HTTPException as WerkzeugHTTPException
3130from app .clients .document_download import DocumentDownloadClient
3231from app .clients .email .aws_ses import AwsSesClient
3332from app .clients .email .aws_ses_stub import AwsSesStubClient
34- from app .clients .pinpoint .aws_pinpoint import AwsPinpointClient
3533from app .clients .sms .aws_sns import AwsSnsClient
3634from notifications_utils import logging , request_helper
3735from notifications_utils .clients .encryption .encryption_client import Encryption
@@ -79,9 +77,9 @@ def apply_driver_hacks(self, app, info, options):
7977 return (sa_url , options )
8078
8179
82- # Set db engine settings here for now.
83- # They were not being set previous (despite environmental variables with appropriate
84- # sounding names) and were defaulting to low values
80+ # no monkey patching issue here. All the real work to set the db up
81+ # is done in db.init_app() which is called in create_app. But we need
82+ # to instantiate the db object here, because it's used in models.py
8583db = SQLAlchemy (
8684 engine_options = {
8785 "pool_size" : config .Config .SQLALCHEMY_POOL_SIZE ,
@@ -91,34 +89,105 @@ def apply_driver_hacks(self, app, info, options):
9189 "pool_pre_ping" : True ,
9290 }
9391)
94- migrate = Migrate ()
92+ migrate = None
93+
94+ # safe to do this for monkeypatching because all real work happens in notify_celery.init_app()
95+ # called in create_app()
9596notify_celery = NotifyCelery ()
96- aws_ses_client = AwsSesClient ()
97- aws_ses_stub_client = AwsSesStubClient ()
98- aws_sns_client = AwsSnsClient ()
99- aws_cloudwatch_client = AwsCloudwatchClient ()
100- aws_pinpoint_client = AwsPinpointClient ()
101- encryption = Encryption ()
102- zendesk_client = ZendeskClient ()
97+ aws_ses_client = None
98+ aws_ses_stub_client = None
99+ aws_sns_client = None
100+ aws_cloudwatch_client = None
101+ encryption = None
102+ zendesk_client = None
103+ # safe to do this for monkeypatching because all real work happens in redis_store.init_app()
104+ # called in create_app()
103105redis_store = RedisClient ()
104- document_download_client = DocumentDownloadClient ()
105-
106- socketio = SocketIO (
107- cors_allowed_origins = [
108- config .Config .ADMIN_BASE_URL ,
109- ],
110- message_queue = config .Config .REDIS_URL ,
111- logger = True ,
112- engineio_logger = True ,
113- )
106+ document_download_client = None
114107
108+ # safe for monkey patching, all work down in
109+ # notification_provider_clients.init_app() in create_app()
115110notification_provider_clients = NotificationProviderClients ()
116111
112+ # LocalProxy doesn't evaluate the target immediately, but defers
113+ # resolution to runtime. So there is no monkeypatching concern.
117114api_user = LocalProxy (lambda : g .api_user )
118115authenticated_service = LocalProxy (lambda : g .authenticated_service )
119116
120117
118+ def get_zendesk_client ():
119+ global zendesk_client
120+ # Our unit tests mock anyway
121+ if os .environ .get ("NOTIFY_ENVIRONMENT" ) == "test" :
122+ return None
123+ if zendesk_client is None :
124+ zendesk_client = ZendeskClient ()
125+ return zendesk_client
126+
127+
128+ def get_aws_ses_client ():
129+ global aws_ses_client
130+ if os .environ .get ("NOTIFY_ENVIRONMENT" ) == "test" :
131+ return AwsSesClient ()
132+ if aws_ses_client is None :
133+ raise RuntimeError (f"Celery not initialized aws_ses_client: { aws_ses_client } " )
134+ return aws_ses_client
135+
136+
137+ def get_aws_sns_client ():
138+ global aws_sns_client
139+ if os .environ .get ("NOTIFY_ENVIRONMENT" ) == "test" :
140+ return AwsSnsClient ()
141+ if aws_ses_client is None :
142+ raise RuntimeError (f"Celery not initialized aws_sns_client: { aws_sns_client } " )
143+ return aws_sns_client
144+
145+
146+ class FakeEncryptionApp :
147+ """
148+ This class is just to support initialization of encryption
149+ during unit tests.
150+ """
151+
152+ config = None
153+
154+ def init_fake_encryption_app (self , config ):
155+ self .config = config
156+
157+
158+ def get_encryption ():
159+ global encryption
160+ if os .environ .get ("NOTIFY_ENVIRONMENT" ) == "test" :
161+ encryption = Encryption ()
162+ fake_app = FakeEncryptionApp ()
163+ sekret = "SEKRET_KEY"
164+ sekret = sekret .replace ("KR" , "CR" )
165+ fake_config = {
166+ "DANGEROUS_SALT" : "SALTYSALTYSALTYSALTY" ,
167+ sekret : "FooFoo" ,
168+ } # noqa
169+ fake_app .init_fake_encryption_app (fake_config )
170+ encryption .init_app (fake_app )
171+ return encryption
172+ if encryption is None :
173+ raise RuntimeError (f"Celery not initialized encryption: { encryption } " )
174+ return encryption
175+
176+
177+ def get_document_download_client ():
178+ global document_download_client
179+ # Our unit tests mock anyway
180+ if os .environ .get ("NOTIFY_ENVIRONMENT" ) == "test" :
181+ return None
182+ if document_download_client is None :
183+ raise RuntimeError (
184+ f"Celery not initialized document_download_client: { document_download_client } "
185+ )
186+ return document_download_client
187+
188+
121189def create_app (application ):
190+ global zendesk_client , migrate , document_download_client , aws_ses_client , aws_ses_stub_client , aws_sns_client , encryption # noqa
122191 from app .config import configs
123192
124193 notify_environment = os .environ ["NOTIFY_ENVIRONMENT" ]
@@ -128,22 +197,35 @@ def create_app(application):
128197 application .config ["NOTIFY_APP_NAME" ] = application .name
129198 init_app (application )
130199
131- socketio .init_app (application )
200+ request_helper .init_app (application )
201+ logging .init_app (application )
132202
133- from app .socket_handlers import register_socket_handlers
203+ # start lazy initialization for gevent
204+ # NOTE: notify_celery and redis_store are safe to construct here
205+ # because all entry points (gunicorn_entry.py, run_celery.py) apply
206+ # monkey.patch_all() first.
207+ # Do NOT access or use them before create_app() is called and don't
208+ # call create_app() in multiple places.
134209
135- register_socket_handlers (socketio )
136- request_helper .init_app (application )
137210 db .init_app (application )
211+
212+ migrate = Migrate ()
138213 migrate .init_app (application , db = db )
214+ if zendesk_client is None :
215+ zendesk_client = ZendeskClient ()
139216 zendesk_client .init_app (application )
140- logging .init_app (application )
141- aws_sns_client .init_app (application )
142-
217+ document_download_client = DocumentDownloadClient ()
218+ document_download_client .init_app (application )
219+ aws_cloudwatch_client = AwsCloudwatchClient ()
220+ aws_cloudwatch_client .init_app (application )
221+ aws_ses_client = AwsSesClient ()
143222 aws_ses_client .init_app ()
223+ aws_ses_stub_client = AwsSesStubClient ()
144224 aws_ses_stub_client .init_app (stub_url = application .config ["SES_STUB_URL" ])
145- aws_cloudwatch_client .init_app (application )
146- aws_pinpoint_client .init_app (application )
225+ aws_sns_client = AwsSnsClient ()
226+ aws_sns_client .init_app (application )
227+ encryption = Encryption ()
228+ encryption .init_app (application )
147229 # If a stub url is provided for SES, then use the stub client rather than the real SES boto client
148230 email_clients = (
149231 [aws_ses_stub_client ]
@@ -153,11 +235,10 @@ def create_app(application):
153235 notification_provider_clients .init_app (
154236 sms_clients = [aws_sns_client ], email_clients = email_clients
155237 )
238+ # end lazy initialization
156239
157240 notify_celery .init_app (application )
158- encryption .init_app (application )
159241 redis_store .init_app (application )
160- document_download_client .init_app (application )
161242
162243 register_blueprint (application )
163244
0 commit comments