5
5
This is a simpler alternative to the plugin-based approach.
6
6
"""
7
7
8
- from typing import Dict , Any , List , Optional
8
+ from typing import Dict , Any , List , Optional , Callable
9
9
import logging
10
- import json
10
+ import time
11
+ import functools
12
+ import threading
11
13
import simple_salesforce
14
+ from simple_salesforce .exceptions import SalesforceExpiredSession
15
+
12
16
from mxcp .runtime import config , on_init , on_shutdown
13
17
14
18
logger = logging .getLogger (__name__ )
15
19
16
20
# Global Salesforce client for reuse across all function calls
17
21
sf_client : Optional [simple_salesforce .Salesforce ] = None
22
+ # Thread lock to protect client initialization
23
+ _client_lock = threading .Lock ()
18
24
19
25
20
26
@on_init
21
27
def setup_salesforce_client ():
22
- """Initialize Salesforce client when server starts."""
23
- global sf_client
24
- logger .info ("Initializing Salesforce client..." )
25
-
26
- sf_config = config .get_secret ("salesforce" )
27
- if not sf_config :
28
- raise ValueError ("Salesforce configuration not found. Please configure Salesforce secrets in your user config." )
29
-
30
- required_keys = ["username" , "password" , "security_token" , "instance_url" , "client_id" ]
31
- missing_keys = [key for key in required_keys if not sf_config .get (key )]
32
- if missing_keys :
33
- raise ValueError (f"Missing Salesforce configuration keys: { ', ' .join (missing_keys )} " )
28
+ """Initialize Salesforce client when server starts.
34
29
35
- sf_client = simple_salesforce .Salesforce (
36
- username = sf_config ["username" ],
37
- password = sf_config ["password" ],
38
- security_token = sf_config ["security_token" ],
39
- instance_url = sf_config ["instance_url" ],
40
- client_id = sf_config ["client_id" ]
41
- )
30
+ Thread-safe: multiple threads can safely call this simultaneously.
31
+ """
32
+ global sf_client
42
33
43
- logger .info ("Salesforce client initialized successfully" )
34
+ with _client_lock :
35
+ logger .info ("Initializing Salesforce client..." )
36
+
37
+ sf_config = config .get_secret ("salesforce" )
38
+ if not sf_config :
39
+ raise ValueError ("Salesforce configuration not found. Please configure Salesforce secrets in your user config." )
40
+
41
+ required_keys = ["username" , "password" , "security_token" , "instance_url" , "client_id" ]
42
+ missing_keys = [key for key in required_keys if not sf_config .get (key )]
43
+ if missing_keys :
44
+ raise ValueError (f"Missing Salesforce configuration keys: { ', ' .join (missing_keys )} " )
45
+
46
+ sf_client = simple_salesforce .Salesforce (
47
+ username = sf_config ["username" ],
48
+ password = sf_config ["password" ],
49
+ security_token = sf_config ["security_token" ],
50
+ instance_url = sf_config ["instance_url" ],
51
+ client_id = sf_config ["client_id" ]
52
+ )
53
+
54
+ logger .info ("Salesforce client initialized successfully" )
44
55
45
56
46
57
@on_shutdown
@@ -53,13 +64,55 @@ def cleanup_salesforce_client():
53
64
logger .info ("Salesforce client cleaned up" )
54
65
55
66
67
+ def retry_on_session_expiration (func : Callable ) -> Callable :
68
+ """
69
+ Decorator that automatically retries functions on session expiration.
70
+
71
+ This only retries on SalesforceExpiredSession, not SalesforceAuthenticationFailed.
72
+ Authentication failures (wrong credentials) should not be retried.
73
+
74
+ Retries up to 2 times on session expiration (3 total attempts).
75
+ Thread-safe: setup_salesforce_client() handles concurrent access internally.
76
+
77
+ Usage:
78
+ @retry_on_session_expiration
79
+ def my_salesforce_function():
80
+ # Function that might fail due to session expiration
81
+ pass
82
+ """
83
+ @functools .wraps (func )
84
+ def wrapper (* args , ** kwargs ):
85
+ max_retries = 2 # Hardcoded: 2 retries = 3 total attempts
86
+
87
+ for attempt in range (max_retries + 1 ):
88
+ try :
89
+ return func (* args , ** kwargs )
90
+ except SalesforceExpiredSession as e :
91
+ if attempt < max_retries :
92
+ logger .warning (f"Session expired on attempt { attempt + 1 } in { func .__name__ } : { e } " )
93
+ logger .info (f"Retrying after re-initializing client (attempt { attempt + 2 } /{ max_retries + 1 } )" )
94
+
95
+ try :
96
+ setup_salesforce_client () # Thread-safe internally
97
+ time .sleep (0.1 ) # Small delay to avoid immediate retry
98
+ except Exception as setup_error :
99
+ logger .error (f"Failed to re-initialize Salesforce client: { setup_error } " )
100
+ raise setup_error # Raise the setup error, not the original session error
101
+ else :
102
+ # Last attempt failed, re-raise the session expiration error
103
+ raise e
104
+
105
+ return wrapper
106
+
107
+
56
108
def _get_salesforce_client () -> simple_salesforce .Salesforce :
57
109
"""Get the global Salesforce client."""
58
110
if sf_client is None :
59
111
raise RuntimeError ("Salesforce client not initialized. Make sure the server is started properly." )
60
112
return sf_client
61
113
62
114
115
+ @retry_on_session_expiration
63
116
def soql (query : str ) -> List [Dict [str , Any ]]:
64
117
"""Execute an SOQL query against Salesforce.
65
118
@@ -84,6 +137,7 @@ def soql(query: str) -> List[Dict[str, Any]]:
84
137
]
85
138
86
139
140
+ @retry_on_session_expiration
87
141
def sosl (query : str ) -> List [Dict [str , Any ]]:
88
142
"""Execute a SOSL query against Salesforce.
89
143
@@ -105,6 +159,7 @@ def sosl(query: str) -> List[Dict[str, Any]]:
105
159
return result .get ('searchRecords' , [])
106
160
107
161
162
+ @retry_on_session_expiration
108
163
def search (search_term : str ) -> List [Dict [str , Any ]]:
109
164
"""Search across all Salesforce objects using a simple search term.
110
165
@@ -125,6 +180,7 @@ def search(search_term: str) -> List[Dict[str, Any]]:
125
180
return sosl (sosl_query )
126
181
127
182
183
+ @retry_on_session_expiration
128
184
def list_sobjects (filter : Optional [str ] = None ) -> List [str ]:
129
185
"""List all available Salesforce objects (sObjects) in the org.
130
186
@@ -136,23 +192,22 @@ def list_sobjects(filter: Optional[str] = None) -> List[str]:
136
192
list: List of Salesforce object names as strings
137
193
"""
138
194
sf = _get_salesforce_client ()
139
-
140
195
describe_result = sf .describe ()
141
-
196
+
142
197
object_names = [obj ['name' ] for obj in describe_result ['sobjects' ]]
143
-
198
+
144
199
if filter is not None and filter .strip ():
145
200
filter_lower = filter .lower ()
146
201
object_names = [
147
202
name for name in object_names
148
203
if filter_lower in name .lower ()
149
204
]
150
-
205
+
151
206
object_names .sort ()
152
-
153
207
return object_names
154
208
155
209
210
+ @retry_on_session_expiration
156
211
def describe_sobject (sobject_name : str ) -> Dict [str , Any ]:
157
212
"""Get the description of a Salesforce object type.
158
213
@@ -195,6 +250,8 @@ def describe_sobject(sobject_name: str) -> Dict[str, Any]:
195
250
196
251
return fields_info
197
252
253
+
254
+ @retry_on_session_expiration
198
255
def get_sobject (sobject_name : str , record_id : str ) -> Dict [str , Any ]:
199
256
"""Get a specific Salesforce object by its ID.
200
257
0 commit comments