Skip to content

Commit 9f7308b

Browse files
committed
mcp-unity: telemetry fire-and-forget; safer sender reg; defer startup/conn telemetry; writer IO logs; manage_scene tolerant params; test worker wake
1 parent 89714d0 commit 9f7308b

File tree

4 files changed

+52
-30
lines changed

4 files changed

+52
-30
lines changed

UnityMcpBridge/Editor/Helpers/TelemetryHelper.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Threading;
34
using UnityEngine;
45

56
namespace MCPForUnity.Editor.Helpers
@@ -124,7 +125,12 @@ public static void RecordEvent(string eventType, Dictionary<string, object> data
124125
/// </summary>
125126
public static void RegisterTelemetrySender(Action<Dictionary<string, object>> sender)
126127
{
127-
s_sender = sender;
128+
Interlocked.Exchange(ref s_sender, sender);
129+
}
130+
131+
public static void UnregisterTelemetrySender()
132+
{
133+
Interlocked.Exchange(ref s_sender, null);
128134
}
129135

130136
/// <summary>
@@ -179,7 +185,7 @@ public static void RecordToolExecution(string toolName, bool success, float dura
179185

180186
private static void SendTelemetryToPythonServer(Dictionary<string, object> telemetryData)
181187
{
182-
var sender = s_sender;
188+
var sender = Volatile.Read(ref s_sender);
183189
if (sender != null)
184190
{
185191
try

UnityMcpBridge/UnityMcpServer~/src/server.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,18 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
6060
server_version = ver_path.read_text(encoding="utf-8").strip()
6161
except Exception:
6262
server_version = "unknown"
63-
# Defer telemetry for first second to avoid interfering with stdio handshake
64-
if (time.perf_counter() - start_clk) > 1.0:
65-
record_telemetry(RecordType.STARTUP, {
66-
"server_version": server_version,
67-
"startup_time": start_time
68-
})
69-
70-
# Record first startup milestone
71-
if (time.perf_counter() - start_clk) > 1.0:
72-
record_milestone(MilestoneType.FIRST_STARTUP)
63+
# Defer initial telemetry by 1s to avoid stdio handshake interference
64+
import threading
65+
def _emit_startup():
66+
try:
67+
record_telemetry(RecordType.STARTUP, {
68+
"server_version": server_version,
69+
"startup_time": start_time,
70+
})
71+
record_milestone(MilestoneType.FIRST_STARTUP)
72+
except Exception:
73+
logger.debug("Deferred startup telemetry failed", exc_info=True)
74+
threading.Timer(1.0, _emit_startup).start()
7375

7476
try:
7577
skip_connect = os.environ.get("UNITY_MCP_SKIP_STARTUP_CONNECT", "").lower() in ("1", "true", "yes", "on")
@@ -79,33 +81,42 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
7981
_unity_connection = get_unity_connection()
8082
logger.info("Connected to Unity on startup")
8183

82-
# Record successful Unity connection
83-
if (time.perf_counter() - start_clk) > 1.0:
84-
record_telemetry(RecordType.UNITY_CONNECTION, {
84+
# Record successful Unity connection (deferred)
85+
import threading as _t
86+
_t.Timer(1.0, lambda: record_telemetry(
87+
RecordType.UNITY_CONNECTION,
88+
{
8589
"status": "connected",
86-
"connection_time_ms": (time.time() - start_time) * 1000
87-
})
90+
"connection_time_ms": (time.perf_counter() - start_clk) * 1000,
91+
}
92+
)).start()
8893

8994
except ConnectionError as e:
9095
logger.warning("Could not connect to Unity on startup: %s", e)
9196
_unity_connection = None
9297

93-
# Record connection failure
94-
if (time.perf_counter() - start_clk) > 1.0:
95-
record_telemetry(RecordType.UNITY_CONNECTION, {
98+
# Record connection failure (deferred)
99+
import threading as _t
100+
_t.Timer(1.0, lambda: record_telemetry(
101+
RecordType.UNITY_CONNECTION,
102+
{
96103
"status": "failed",
97104
"error": str(e)[:200],
98-
"connection_time_ms": (time.perf_counter() - start_clk) * 1000
99-
})
105+
"connection_time_ms": (time.perf_counter() - start_clk) * 1000,
106+
}
107+
)).start()
100108
except Exception as e:
101109
logger.warning("Unexpected error connecting to Unity on startup: %s", e)
102110
_unity_connection = None
103-
if (time.perf_counter() - start_clk) > 1.0:
104-
record_telemetry(RecordType.UNITY_CONNECTION, {
111+
import threading as _t
112+
_t.Timer(1.0, lambda: record_telemetry(
113+
RecordType.UNITY_CONNECTION,
114+
{
105115
"status": "failed",
106116
"error": str(e)[:200],
107-
"connection_time_ms": (time.perf_counter() - start_clk) * 1000
108-
})
117+
"connection_time_ms": (time.perf_counter() - start_clk) * 1000,
118+
}
119+
)).start()
109120

110121
try:
111122
# Yield the connection object so it can be attached to the context

UnityMcpBridge/UnityMcpServer~/src/telemetry.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,10 @@ def __init__(self):
169169
self._lock: threading.Lock = threading.Lock()
170170
# Bounded queue with single background worker (records only; no context propagation)
171171
self._queue: "queue.Queue[TelemetryRecord]" = queue.Queue(maxsize=1000)
172+
# Load persistent data before starting worker so first events have UUID
173+
self._load_persistent_data()
172174
self._worker: threading.Thread = threading.Thread(target=self._worker_loop, daemon=True)
173175
self._worker.start()
174-
self._load_persistent_data()
175176

176177
def _load_persistent_data(self):
177178
"""Load UUID and milestones from disk"""
@@ -307,8 +308,8 @@ def _send_telemetry(self, record: TelemetryRecord):
307308
# Re-validate endpoint at send time to handle dynamic changes
308309
endpoint = self.config._validated_endpoint(self.config.endpoint, self.config.default_endpoint)
309310
response = client.post(endpoint, json=payload)
310-
if response.status_code == 200:
311-
logger.info(f"Telemetry sent: {record.record_type}")
311+
if 200 <= response.status_code < 300:
312+
logger.debug(f"Telemetry sent: {record.record_type}")
312313
else:
313314
logger.warning(f"Telemetry failed: HTTP {response.status_code}")
314315
else:
@@ -325,7 +326,7 @@ def _send_telemetry(self, record: TelemetryRecord):
325326
try:
326327
with urllib.request.urlopen(req, timeout=self.config.timeout) as resp:
327328
if 200 <= resp.getcode() < 300:
328-
logger.info(f"Telemetry sent (urllib): {record.record_type}")
329+
logger.debug(f"Telemetry sent (urllib): {record.record_type}")
329330
else:
330331
logger.warning(f"Telemetry failed (urllib): HTTP {resp.getcode()}")
331332
except urllib.error.URLError as ue:

tests/test_telemetry_queue_worker.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,13 @@ def test_telemetry_queue_backpressure_and_single_worker(monkeypatch, caplog):
4545
# Force-enable telemetry regardless of env settings from conftest
4646
collector.config.enabled = True
4747

48+
# Wake existing worker once so it observes the new queue on the next loop
49+
collector.record(telemetry.RecordType.TOOL_EXECUTION, {"i": -1})
4850
# Replace queue with tiny one to trigger backpressure quickly
4951
small_q = q.Queue(maxsize=2)
5052
collector._queue = small_q
53+
# Give the worker a moment to switch queues
54+
time.sleep(0.02)
5155

5256
# Make sends slow to build backlog and exercise worker
5357
def slow_send(self, rec):

0 commit comments

Comments
 (0)