Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.9.21 (2025-10-16)

### Changes

- Warn callers when they use the webstack client from different threads.

## 0.9.20 (2025-10-10)

### Changes
Expand Down
26 changes: 25 additions & 1 deletion python/mujinwebstackclient/controllerwebclientraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,20 @@ class ControllerWebClientRaw(object):
_subscriptionLock: threading.Lock # Lock protecting _webSocket and _subscriptions
_backgroundThread: BackgroundThread = None # The background thread to handle async operations

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:
_threadName: Optional[str] = None # The last thread this client was used in if we're warning on calls from different threads.

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
Expand Down Expand Up @@ -201,6 +214,11 @@ def __init__(self, baseurl: str, username: str, password: str, locale: Optional[
# Set user agent header
self.SetUserAgent(userAgent)

if warnOnUseFromDifferentThreads:
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()

Expand Down Expand Up @@ -262,6 +280,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().name
if currentName != self._threadName:
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)

# in verbose logging, log the caller
Expand Down
2 changes: 1 addition & 1 deletion python/mujinwebstackclient/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '0.9.20'
__version__ = '0.9.21'

# Do not forget to update CHANGELOG.md
27 changes: 24 additions & 3 deletions python/mujinwebstackclient/webstackclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -652,7 +673,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')))
Expand Down