From 339f31ec066a05babc8febb6f63ec569540bc902 Mon Sep 17 00:00:00 2001 From: "heman.gandhi" Date: Thu, 25 Sep 2025 19:48:38 +0900 Subject: [PATCH 1/9] feat: warn about calls from different threads --- .../mujinwebstackclient/controllerwebclientraw.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/python/mujinwebstackclient/controllerwebclientraw.py b/python/mujinwebstackclient/controllerwebclientraw.py index 6326264..89aef73 100644 --- a/python/mujinwebstackclient/controllerwebclientraw.py +++ b/python/mujinwebstackclient/controllerwebclientraw.py @@ -20,6 +20,7 @@ import traceback import uuid import copy +import threading import websockets from requests import auth as requests_auth from requests import adapters as requests_adapters @@ -157,6 +158,8 @@ class ControllerWebClientRaw(object): _subscriptionLock: threading.Lock # Lock protecting _webSocket and _subscriptions _backgroundThread: BackgroundThread = None # The background thread to handle async operations + _threadName: Optional[str] = None + def __init__(self, baseurl: str, username: str, password: str, locale: Optional[str] = None, author: Optional[str] = None, userAgent: Optional[str] = None, additionalHeaders: Optional[Dict[str, str]] = None, unixEndpoint: Optional[str] = None) -> None: self._baseurl = baseurl self._username = username @@ -201,6 +204,12 @@ def __init__(self, baseurl: str, username: str, password: str, locale: Optional[ # Set user agent header self.SetUserAgent(userAgent) + if os.getenv("MUJIN_WEBSTACK_CLIENT_WARN_ON_MULTIPLE_CALLERS") == "true": + self._threadName = threading.current_thread().getName() + log.info( + "Initialized webstack client with warning on calls from different threads enabled. This may degrade performance. Set MUJIN_WEBSTACK_CLIENT_WARN_ON_MULTIPLE_CALLERS to 'false' to disable this if performance is too poor." + ) + def __del__(self): self.Destroy() @@ -262,6 +271,12 @@ def Request( # by default, disallow redirect since DELETE with redirection is too dangerous kwargs['allow_redirects'] = method in ('GET',) + if self._threadName is not None: + currentName = threading.current_thread().getName() + if currentName != self._threadName: + log.warning("The webstack client has been called across multiple threads! Was %s, now %s.", self._threadName, currentName) + self._threadName = currentName + response = self._session.request(method=method, url=url, timeout=timeout, headers=headers, **kwargs) # in verbose logging, log the caller From 1048dbc86c06f3aca4a9a07a73a7a38e0803a737 Mon Sep 17 00:00:00 2001 From: "heman.gandhi" Date: Thu, 25 Sep 2025 19:49:11 +0900 Subject: [PATCH 2/9] refactor: fix type annotation --- python/mujinwebstackclient/webstackclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mujinwebstackclient/webstackclient.py b/python/mujinwebstackclient/webstackclient.py index cd7dd42..e602d54 100644 --- a/python/mujinwebstackclient/webstackclient.py +++ b/python/mujinwebstackclient/webstackclient.py @@ -652,7 +652,7 @@ def DeleteJobs(self, timeout=5): # def CreateLogEntries(self, logEntries, timeout=5): - # type: (List[Tuple[str, Any, Dict[str, bytes]]], int) -> Any + # type: (List[Tuple[str, Any, Dict[str, bytes]]], float) -> Any files = [] for logType, logEntry, attachments in logEntries: files.append(('logEntry/%s' % logType, ('', json.dumps(logEntry), 'application/json'))) From 576b10c09f4b65859ea43fc73cd9f2a2b556e9fe Mon Sep 17 00:00:00 2001 From: "heman.gandhi" Date: Tue, 30 Sep 2025 17:48:44 +0900 Subject: [PATCH 3/9] chore: bump version and log change --- CHANGELOG.md | 6 ++++++ python/mujinwebstackclient/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1b352e..6fa8b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.9.20 (2025-09-30) + +### Changes + +- Warn callers when they use the webstack client from different threads. + ## 0.9.19 (2025-09-25) ### Changes diff --git a/python/mujinwebstackclient/version.py b/python/mujinwebstackclient/version.py index 2b53354..ff3b060 100644 --- a/python/mujinwebstackclient/version.py +++ b/python/mujinwebstackclient/version.py @@ -1,3 +1,3 @@ -__version__ = '0.9.19' +__version__ = '0.9.20' # Do not forget to update CHANGELOG.md From 7ef096674a7705f0451a474f679d7cbae48be41b Mon Sep 17 00:00:00 2001 From: "heman.gandhi" Date: Tue, 30 Sep 2025 17:51:43 +0900 Subject: [PATCH 4/9] lint: deduplicate imports --- python/mujinwebstackclient/controllerwebclientraw.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/mujinwebstackclient/controllerwebclientraw.py b/python/mujinwebstackclient/controllerwebclientraw.py index 89aef73..a7aa900 100644 --- a/python/mujinwebstackclient/controllerwebclientraw.py +++ b/python/mujinwebstackclient/controllerwebclientraw.py @@ -20,7 +20,6 @@ import traceback import uuid import copy -import threading import websockets from requests import auth as requests_auth from requests import adapters as requests_adapters From 50a160720f4f003f111d1d319eb3289f3da1b4c3 Mon Sep 17 00:00:00 2001 From: "heman.gandhi" Date: Wed, 1 Oct 2025 14:11:53 +0900 Subject: [PATCH 5/9] refactor: accept whether to warn as a parameter in the constructors --- .../controllerwebclientraw.py | 15 +++++++++-- python/mujinwebstackclient/webstackclient.py | 25 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/python/mujinwebstackclient/controllerwebclientraw.py b/python/mujinwebstackclient/controllerwebclientraw.py index a7aa900..baf0fee 100644 --- a/python/mujinwebstackclient/controllerwebclientraw.py +++ b/python/mujinwebstackclient/controllerwebclientraw.py @@ -159,7 +159,18 @@ class ControllerWebClientRaw(object): _threadName: Optional[str] = None - def __init__(self, baseurl: str, username: str, password: str, locale: Optional[str] = None, author: Optional[str] = None, userAgent: Optional[str] = None, additionalHeaders: Optional[Dict[str, str]] = None, unixEndpoint: Optional[str] = None) -> None: + def __init__( + self, + baseurl: str, + username: str, + password: str, + locale: Optional[str] = None, + author: Optional[str] = None, + userAgent: Optional[str] = None, + additionalHeaders: Optional[Dict[str, str]] = None, + unixEndpoint: Optional[str] = None, + warnOnUseFromDifferentThreads: bool = False, + ) -> None: self._baseurl = baseurl self._username = username self._password = password @@ -203,7 +214,7 @@ def __init__(self, baseurl: str, username: str, password: str, locale: Optional[ # Set user agent header self.SetUserAgent(userAgent) - if os.getenv("MUJIN_WEBSTACK_CLIENT_WARN_ON_MULTIPLE_CALLERS") == "true": + if warnOnUseFromDifferentThreads: self._threadName = threading.current_thread().getName() log.info( "Initialized webstack client with warning on calls from different threads enabled. This may degrade performance. Set MUJIN_WEBSTACK_CLIENT_WARN_ON_MULTIPLE_CALLERS to 'false' to disable this if performance is too poor." diff --git a/python/mujinwebstackclient/webstackclient.py b/python/mujinwebstackclient/webstackclient.py index 90a3a46..c44ecff 100644 --- a/python/mujinwebstackclient/webstackclient.py +++ b/python/mujinwebstackclient/webstackclient.py @@ -119,7 +119,17 @@ def offset(self): controllerIp = '' # Hostname of the controller web server controllerPort = 80 # Port of the controller web server - def __init__(self, controllerurl='http://127.0.0.1', controllerusername='', controllerpassword='', author=None, userAgent=None, additionalHeaders=None, unixEndpoint=None): + def __init__( + self, + controllerurl='http://127.0.0.1', + controllerusername='', + controllerpassword='', + author=None, + userAgent=None, + additionalHeaders=None, + unixEndpoint=None, + warnOnUseFromDifferentThreads: bool = False, + ): """Logs into the Mujin controller. Args: @@ -129,6 +139,8 @@ def __init__(self, controllerurl='http://127.0.0.1', controllerusername='', cont userAgent (str): User agent to be sent on each request additionalHeaders (dict): Additional HTTP headers to be included in requests unixEndpoint (str): Unix socket endpoint for communicating with HTTP server over unix socket + warnOnUseFromDifferentThreads (bool): Whether to warn callers if the client is used from different threads. + Defaults to not warning since checking the thread name on each call may significantly degrade performance. """ # Parse controllerurl @@ -155,7 +167,16 @@ def __init__(self, controllerurl='http://127.0.0.1', controllerusername='', cont 'username': self.controllerusername, 'locale': os.environ.get('LANG', ''), } - self._webclient = controllerwebclientraw.ControllerWebClientRaw(self.controllerurl, self.controllerusername, self.controllerpassword, author=author, userAgent=userAgent, additionalHeaders=additionalHeaders, unixEndpoint=unixEndpoint) + self._webclient = controllerwebclientraw.ControllerWebClientRaw( + self.controllerurl, + self.controllerusername, + self.controllerpassword, + author=author, + userAgent=userAgent, + additionalHeaders=additionalHeaders, + unixEndpoint=unixEndpoint, + warnOnUseFromDifferentThreads=warnOnUseFromDifferentThreads, + ) def __del__(self): self.Destroy() From 0b276ec5548277f63f4217b3a4f7f2f586d65cda Mon Sep 17 00:00:00 2001 From: "heman.gandhi" Date: Thu, 2 Oct 2025 14:58:02 +0900 Subject: [PATCH 6/9] docs: document the new member variable --- python/mujinwebstackclient/controllerwebclientraw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mujinwebstackclient/controllerwebclientraw.py b/python/mujinwebstackclient/controllerwebclientraw.py index baf0fee..7e626e8 100644 --- a/python/mujinwebstackclient/controllerwebclientraw.py +++ b/python/mujinwebstackclient/controllerwebclientraw.py @@ -157,7 +157,7 @@ class ControllerWebClientRaw(object): _subscriptionLock: threading.Lock # Lock protecting _webSocket and _subscriptions _backgroundThread: BackgroundThread = None # The background thread to handle async operations - _threadName: Optional[str] = None + _threadName: Optional[str] = None # The last thread this client was used in if we're warning on calls from different threads. def __init__( self, From 734a7704086997cd19e54ca2d5b187858882a5e6 Mon Sep 17 00:00:00 2001 From: "heman.gandhi" Date: Thu, 2 Oct 2025 14:58:17 +0900 Subject: [PATCH 7/9] fix: clarify and correct logs --- python/mujinwebstackclient/controllerwebclientraw.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/python/mujinwebstackclient/controllerwebclientraw.py b/python/mujinwebstackclient/controllerwebclientraw.py index 7e626e8..59fbcdc 100644 --- a/python/mujinwebstackclient/controllerwebclientraw.py +++ b/python/mujinwebstackclient/controllerwebclientraw.py @@ -217,7 +217,7 @@ def __init__( if warnOnUseFromDifferentThreads: self._threadName = threading.current_thread().getName() log.info( - "Initialized webstack client with warning on calls from different threads enabled. This may degrade performance. Set MUJIN_WEBSTACK_CLIENT_WARN_ON_MULTIPLE_CALLERS to 'false' to disable this if performance is too poor." + "Initialized webstack client with warning on calls from different threads enabled. This may degrade performance. Set warnOnUseFromDifferentThreads to 'False' to disable this if performance is too poor." ) def __del__(self): @@ -284,7 +284,11 @@ def Request( if self._threadName is not None: currentName = threading.current_thread().getName() if currentName != self._threadName: - log.warning("The webstack client has been called across multiple threads! Was %s, now %s.", self._threadName, currentName) + log.warning( + "The webstack client has been called across multiple threads! Was \"%s\", now \"%s\".", + self._threadName, + currentName, + ) self._threadName = currentName response = self._session.request(method=method, url=url, timeout=timeout, headers=headers, **kwargs) From a80f155f46b1487d18f2851a0736b623e5ad5b49 Mon Sep 17 00:00:00 2001 From: Barkin Simsek Date: Fri, 17 Oct 2025 08:56:41 +0900 Subject: [PATCH 8/9] Run formatter. --- CHANGELOG.md | 1 + python/mujinwebstackclient/controllerwebclientraw.py | 4 ++-- python/mujinwebstackclient/webstackgraphclient.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fa9b1f..2876d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changes - Warn callers when they use the webstack client from different threads. + ## 0.9.20 (2025-10-10) - Re-generate graph api. diff --git a/python/mujinwebstackclient/controllerwebclientraw.py b/python/mujinwebstackclient/controllerwebclientraw.py index 59fbcdc..8592da2 100644 --- a/python/mujinwebstackclient/controllerwebclientraw.py +++ b/python/mujinwebstackclient/controllerwebclientraw.py @@ -217,7 +217,7 @@ def __init__( if warnOnUseFromDifferentThreads: self._threadName = threading.current_thread().getName() log.info( - "Initialized webstack client with warning on calls from different threads enabled. This may degrade performance. Set warnOnUseFromDifferentThreads to 'False' to disable this if performance is too poor." + "Initialized webstack client with warning on calls from different threads enabled. This may degrade performance. Set warnOnUseFromDifferentThreads to 'False' to disable this if performance is too poor.", ) def __del__(self): @@ -285,7 +285,7 @@ def Request( currentName = threading.current_thread().getName() if currentName != self._threadName: log.warning( - "The webstack client has been called across multiple threads! Was \"%s\", now \"%s\".", + 'The webstack client has been called across multiple threads! Was "%s", now "%s".', self._threadName, currentName, ) diff --git a/python/mujinwebstackclient/webstackgraphclient.py b/python/mujinwebstackclient/webstackgraphclient.py index 0fdf8bf..931ac11 100644 --- a/python/mujinwebstackclient/webstackgraphclient.py +++ b/python/mujinwebstackclient/webstackgraphclient.py @@ -1164,7 +1164,7 @@ def GetLogEntry( Returns: LogEntry: An entry in the logs. The current parent-children level relationship among log entry types: - + ``` LogEntry ├─ GenericLogEntry From d01492141752f45ef8cad86d3ca4a0ab95b000d0 Mon Sep 17 00:00:00 2001 From: Barkin Simsek Date: Fri, 17 Oct 2025 09:02:30 +0900 Subject: [PATCH 9/9] Stylistic nitpicks. --- .../mujinwebstackclient/controllerwebclientraw.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/python/mujinwebstackclient/controllerwebclientraw.py b/python/mujinwebstackclient/controllerwebclientraw.py index 8592da2..0a861ba 100644 --- a/python/mujinwebstackclient/controllerwebclientraw.py +++ b/python/mujinwebstackclient/controllerwebclientraw.py @@ -215,10 +215,9 @@ def __init__( self.SetUserAgent(userAgent) if warnOnUseFromDifferentThreads: - self._threadName = threading.current_thread().getName() - log.info( - "Initialized webstack client with warning on calls from different threads enabled. This may degrade performance. Set warnOnUseFromDifferentThreads to 'False' to disable this if performance is too poor.", - ) + self._threadName = threading.current_thread().name + log.info('initialized client with warning on calls from different threads enabled and this may degrade performance') + log.info('set "warnOnUseFromDifferentThreads" to "False" to disable this if performance is poor') def __del__(self): self.Destroy() @@ -282,13 +281,9 @@ def Request( kwargs['allow_redirects'] = method in ('GET',) if self._threadName is not None: - currentName = threading.current_thread().getName() + currentName = threading.current_thread().name if currentName != self._threadName: - log.warning( - 'The webstack client has been called across multiple threads! Was "%s", now "%s".', - self._threadName, - currentName, - ) + log.warning('client has been called across multiple threads, was "%s", now "%s"', self._threadName, currentName) self._threadName = currentName response = self._session.request(method=method, url=url, timeout=timeout, headers=headers, **kwargs)