1818"""Client for remote services"""
1919
2020import logging
21+ import time
2122from contextlib import contextmanager
2223from dataclasses import MISSING , dataclass
2324from typing import Dict , List
@@ -41,6 +42,8 @@ class RemoteSvcConfig:
4142 provision_params_tmpl : Dict
4243 jwt_auth_conf : Dict
4344 prefer_async_delete : bool = True
45+ # 是否使用幂等创建实例接口
46+ prefer_idempotent_provision : bool = False
4447 is_ready : bool = True
4548
4649 @classmethod
@@ -86,6 +89,7 @@ def __post_init__(self):
8689 self .retrieve_instance_by_name_url = urljoin (self .endpoint_url , "services/{service_id}/instances/?name={name}" )
8790 self .update_inst_config_url = urljoin (self .endpoint_url , "instances/{instance_id}/config/" )
8891 self .create_instance_url = urljoin (self .endpoint_url , "services/{service_id}/instances/{instance_id}/" )
92+ self .idem_prov = urljoin (self .endpoint_url , "services/{service_id}/instances/idem_prov/" )
8993 self .delete_instance_url = urljoin (self .endpoint_url , "instances/{instance_id}/" )
9094 self .async_delete_instance_url = urljoin (self .endpoint_url , "instances/{instance_id}/async_delete" )
9195 # 增强服务绑定
@@ -132,6 +136,25 @@ def validate_resp(resp: requests.Response):
132136 response_text = resp .text ,
133137 )
134138
139+ def _post_until_ready (self , url : str , payload : Dict ) -> requests .Response :
140+ """Retry POST requests until the async endpoint returns 200/201 or timeout."""
141+ deadline = time .monotonic () + self .REQUEST_CREATE_TIMEOUT
142+
143+ poll_interval = 0.5
144+ while time .monotonic () < deadline :
145+ resp = requests .post (url , json = payload , auth = self .auth , timeout = self .REQUEST_CREATE_TIMEOUT )
146+ self .validate_resp (resp )
147+ # 200,201 表示资源已就绪,可以返回了
148+ # 202,表示请求已接受,但资源未就绪,需要继续轮询
149+ if resp .status_code in {200 , 201 }:
150+ return resp
151+ time .sleep (poll_interval )
152+ poll_interval = min (poll_interval * 2 , 4 )
153+
154+ raise RemoteClientError (
155+ f"POST to { desensitize_url (url )} not ready after retrying for { self .REQUEST_CREATE_TIMEOUT } seconds"
156+ )
157+
135158 def get_meta_info (self ) -> Dict :
136159 """Get service's meta info
137160
@@ -214,6 +237,19 @@ def provision_instance(self, service_id: str, plan_id: str, instance_id: str, pa
214237 self .validate_resp (resp )
215238 return resp .json ()
216239
240+ def idempotent_provision_instance (self , service_id : str , plan_id : str , params : Dict ) -> Dict :
241+ """Idempotently provision a new instance,
242+ `params['engine_app_name']` treated as the idempotency key, so it's required
243+
244+ :raises: RemoteClientError
245+ :return: <instance dict>
246+ """
247+ url = self .config .idem_prov .format (service_id = service_id )
248+ payload = {"plan_id" : plan_id , "params" : params }
249+ with wrap_request_exc (self ):
250+ resp = self ._post_until_ready (url , payload )
251+ return resp .json ()
252+
217253 def retrieve_instance (self , instance_id : str ) -> Dict :
218254 """Retrieve a provisioned instance info
219255
@@ -291,8 +327,7 @@ def update_instance_config(self, instance_id: str, config: Dict):
291327 def create_client_side_instance (self , service_id : str , instance_id : str , params : Dict ):
292328 url = self .config .create_client_side_instance_url .format (service_id = service_id , instance_id = instance_id )
293329 with wrap_request_exc (self ):
294- resp = requests .post (url , json = params , auth = self .auth , timeout = self .REQUEST_CREATE_TIMEOUT )
295- self .validate_resp (resp )
330+ resp = self ._post_until_ready (url , params )
296331 return resp .json ()
297332
298333 def destroy_client_side_instance (self , instance_id : str ):
0 commit comments