commandnet 0.5.0__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commandnet
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: A lightweight, Pydantic-powered, distributed event-driven state machine and typed node graph runtime.
5
5
  Author: Christopher Vaz
6
6
  Author-email: christophervaz160@gmail.com
@@ -70,8 +70,10 @@ class Engine:
70
70
 
71
71
  async def _run_node_logic(self, event: Event):
72
72
  subject_id = event.subject_id
73
- # We allow "AWAITING_CALL" because an subject wakes up from that state when a 'Call' resolves
73
+
74
+ # 1. READ & LOCK
74
75
  node_name, ctx_dict = await self.db.lock_and_load(subject_id)
76
+
75
77
  if not node_name or (
76
78
  node_name != event.node_name and node_name != "AWAITING_CALL"
77
79
  ):
@@ -92,33 +94,42 @@ class Engine:
92
94
  if hasattr(ctx_type, "model_validate")
93
95
  else ctx_dict
94
96
  )
97
+
95
98
  payload = (
96
99
  payload_type.model_validate(event.payload)
97
100
  if (event.payload and hasattr(payload_type, "model_validate"))
98
101
  else event.payload
99
102
  )
100
103
 
101
- # Support Soft-Cancel checking inside Node.run
104
+ # Soft cancel check BEFORE compute
102
105
  if hasattr(ctx, "is_cancelled"):
103
106
  ctx.is_cancelled = await self.db.is_cancelled(subject_id)
104
107
 
108
+ # 2. RELEASE LOCK before long-running compute
109
+ await self.db.unlock_subject(subject_id)
110
+
111
+ # 3. COMPUTE (no DB lock held)
105
112
  start_t = asyncio.get_event_loop().time()
106
113
  result = await node_cls().run(ctx, payload)
114
+ duration = (asyncio.get_event_loop().time() - start_t) * 1000
115
+
116
+ # 4. RE-LOCK before writing
117
+ node_name_check, _ = await self.db.lock_and_load(subject_id)
118
+
119
+ # 🔴 IMPORTANT SAFETY CHECK (prevents stale writes)
120
+ if node_name_check != event.node_name and node_name_check != "AWAITING_CALL":
121
+ await self.db.unlock_subject(subject_id)
122
+ return
123
+
124
+ await self._apply_target(subject_id, ctx, result, duration)
107
125
 
108
- await self._apply_target(
109
- subject_id,
110
- ctx,
111
- result,
112
- (asyncio.get_event_loop().time() - start_t) * 1000,
113
- )
114
126
  except Exception as e:
115
127
  await self.observer.on_error(subject_id, event.node_name, e)
116
128
  raise
117
129
  finally:
130
+ # Ensure we never leave a lock hanging
118
131
  await self.db.unlock_subject(subject_id)
119
132
 
120
- # --- RECURSIVE TARGET RESOLVER ---
121
-
122
133
  async def _apply_target(
123
134
  self,
124
135
  subject_id: str,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "commandnet"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "A lightweight, Pydantic-powered, distributed event-driven state machine and typed node graph runtime."
5
5
  authors = [
6
6
  { name = "Christopher Vaz", email = "christophervaz160@gmail.com" }
File without changes