thds.mops 3.9.20250929214325__py3-none-any.whl → 3.9.20250930235847__py3-none-any.whl

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.

Potentially problematic release.


This version of thds.mops might be problematic. Click here for more details.

@@ -44,20 +44,20 @@ class _FutureInterpretationShim(ty.Generic[R_0, R]):
44
44
  def __hash__(self) -> int:
45
45
  return hash(self._id)
46
46
 
47
- def __call__(self, r_0: ty.Optional[R_0], last_seen_at: float) -> ty.Optional[Self]:
47
+ def interpret(self, r_0: ty.Optional[R_0], last_seen_at: float) -> ty.Optional[Self]:
48
48
  """First and foremost - this _must_ be treated as an object that the creator
49
49
  is ultimately responsible for calling on a semi-regular basis. It represents a
50
50
  likely deadlock for the holder of the Future if it is never called.
51
51
 
52
- Return False if the Future is still in progress and should not be unregistered.
53
- Return True if the Future is done and should be unregistered.
52
+ Return None if the Future is still in progress and should not be unregistered.
53
+ Return self if the Future is done and should be unregistered.
54
54
  """
55
55
  try:
56
56
  interpretation = self._interpreter(r_0, last_seen_at)
57
57
  if isinstance(interpretation, NotYetDone):
58
58
  return None # do nothing and do not unregister - the status is still in progress.
59
59
 
60
- self.future.set_result(interpretation)
60
+ self.future.set_result(interpretation) # resolved successfully!
61
61
  except Exception as e:
62
62
  self.future.set_exception(e)
63
63
 
@@ -71,7 +71,7 @@ K = ty.TypeVar("K") # Key type for the UncertainFuturesTracker
71
71
  class _FuturesState(ty.Generic[R_0]):
72
72
  """Represents a single 'observable' that may have multiple Futures (and therefore interpretations) associated with it."""
73
73
 
74
- futshims: list[_FutureInterpretationShim[R_0, ty.Any]]
74
+ futshims: set[_FutureInterpretationShim[R_0, ty.Any]]
75
75
  last_seen_at: float
76
76
 
77
77
 
@@ -94,10 +94,16 @@ class UncertainFuturesTracker(ty.Generic[K, R_0]):
94
94
  never resolve the Future, then a caller may be waiting for it forever. Therefore, we
95
95
  ask the original requestor of the Future to specify how long they are willing to wait
96
96
  to get a result, after which point we will resolve the Future as an exception.
97
+
98
+ Notably, once we have seen an object, we will not ever remove it from our tracking list.
99
+ This implies a certain amount of memory growth over time, but it avoids race conditions
100
+ between producers and consumers of the Futures.
97
101
  """
98
102
 
99
103
  def __init__(self, allowed_stale_seconds: float) -> None:
100
104
  self._keyed_futures_state = collections.OrderedDict[K, _FuturesState[R_0]]()
105
+ # ordered from least-recently-seen to most-recently-seen, so that we can easily garbage collect
106
+ # potentially stale Futures, which will be at the front of the OrderedDict.
101
107
  self._lock = threading.Lock() # i don't trust ordered dict operations to be thread-safe.
102
108
  self._check_stale_seconds = allowed_stale_seconds
103
109
 
@@ -105,16 +111,11 @@ class UncertainFuturesTracker(ty.Generic[K, R_0]):
105
111
  futshim = _FutureInterpretationShim(interpreter)
106
112
  with self._lock:
107
113
  if key not in self._keyed_futures_state:
108
- self._keyed_futures_state[key] = _FuturesState(
109
- [futshim],
110
- last_seen_at=official_timer() + self._check_stale_seconds,
111
- # we provide a double margin for objects that we have never seen before.
112
- )
113
- self._keyed_futures_state.move_to_end(key, last=False)
114
- # never seen and therefore should be at the beginning (most stale)
114
+ self._keyed_futures_state[key] = _FuturesState({futshim}, last_seen_at=official_timer())
115
+ # never seen and therefore should be at the end (least stale)
115
116
  else:
116
117
  # maintain our ordered dict so we can handle garbage collection of stale Futures.
117
- self._keyed_futures_state[key].futshims.append(futshim)
118
+ self._keyed_futures_state[key].futshims.add(futshim)
118
119
 
119
120
  return futshim.future
120
121
 
@@ -126,10 +127,10 @@ class UncertainFuturesTracker(ty.Generic[K, R_0]):
126
127
  If `key` is None, we will update all Futures that have been created so far.
127
128
  """
128
129
 
129
- def check_resolution(fut_state: _FuturesState[R_0], inner_r_0: ty.Optional[R_0]) -> None:
130
+ def interpret_event(fut_state: _FuturesState[R_0], inner_r_0: ty.Optional[R_0]) -> None:
130
131
  for future_shim_that_is_done in core.parallel.yield_results(
131
132
  [
132
- core.thunks.thunking(futshim)(inner_r_0, fut_state.last_seen_at)
133
+ core.thunks.thunking(futshim.interpret)(inner_r_0, fut_state.last_seen_at)
133
134
  for futshim in fut_state.futshims
134
135
  ],
135
136
  progress_logger=core.log.getLogger(__name__).debug,
@@ -137,24 +138,33 @@ class UncertainFuturesTracker(ty.Generic[K, R_0]):
137
138
  ):
138
139
  if future_shim_that_is_done is not None:
139
140
  # the Future is done, so we can remove it from the list of Futures.
140
- fut_state.futshims.remove(future_shim_that_is_done)
141
+ fut_state.futshims.discard(future_shim_that_is_done)
141
142
 
142
143
  if key is not None:
143
144
  with self._lock:
144
145
  if key not in self._keyed_futures_state:
145
- self._keyed_futures_state[key] = _FuturesState(list(), last_seen_at=official_timer())
146
+ self._keyed_futures_state[key] = _FuturesState(set(), last_seen_at=official_timer())
146
147
  else:
147
148
  # maintain our ordered dict so we can handle garbage collection of stale Futures.
148
149
  self._keyed_futures_state.move_to_end(key)
149
150
  self._keyed_futures_state[key].last_seen_at = official_timer()
150
151
 
151
- fut_state = self._keyed_futures_state[key]
152
- check_resolution(fut_state, r_0)
152
+ if fut_state := self._keyed_futures_state.get(key):
153
+ interpret_event(fut_state, r_0)
153
154
 
155
+ #
154
156
  # 'garbage collect' any Futures that haven't been updated in a while.
155
- for futs_state in self._keyed_futures_state.values():
156
- if futs_state.last_seen_at + self._check_stale_seconds < official_timer():
157
- check_resolution(futs_state, None)
157
+ #
158
+ with self._lock:
159
+ safe_futures = list(self._keyed_futures_state.values())
160
+ # this avoids holding the lock, but also avoids RuntimeError: OrderedDict mutated during iteration
161
+ now = official_timer()
162
+ for futs_state in safe_futures:
163
+ if now > futs_state.last_seen_at + self._check_stale_seconds:
164
+ interpret_event(futs_state, None)
165
+ # None means we have no new information about the object.
166
+ # the interpreter must decide what to do with that, plus the last seen time.
167
+
158
168
  else: # these are ordered, so once we see one that's not stale, we can stop checking.
159
169
  # this prevents us from having to do O(N) checks for every update.
160
170
  break
thds/mops/k8s/watch.py CHANGED
@@ -112,9 +112,14 @@ def callback_events(
112
112
  ) -> None:
113
113
  """Suitable for use with a daemon thread."""
114
114
  for namespace, obj, event in event_yielder:
115
- should_exit = on_event(namespace, obj, event)
116
- if should_exit:
117
- break
115
+ try:
116
+ should_exit = on_event(namespace, obj, event)
117
+ if should_exit:
118
+ break
119
+ except Exception:
120
+ logger.exception(
121
+ "Exception in k8s watch event callback [probably a bug in mops] - continuing..."
122
+ )
118
123
 
119
124
 
120
125
  def _default_get_name(obj: ty.Any) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thds.mops
3
- Version: 3.9.20250929214325
3
+ Version: 3.9.20250930235847
4
4
  Summary: ML Ops tools for Trilliant Health
5
5
  Author-email: Trilliant Health <info@trillianthealth.com>
6
6
  Project-URL: Repository, https://github.com/TrilliantHealth/ds-monorepo
@@ -31,10 +31,10 @@ thds/mops/k8s/namespace.py,sha256=Z6trVTU9WFashto4PqIhTcxu-foOF93W0TpgqCU7WIA,38
31
31
  thds/mops/k8s/node_selection.py,sha256=Gy2Jz8IxZblg2LmtGg8-MtKI4RmXz2AMXqFPP8OQyu0,2065
32
32
  thds/mops/k8s/retry.py,sha256=JVfP304kItpLs5nrONHE5UWkVWlrFGlV_oFQqhq3zHg,2846
33
33
  thds/mops/k8s/too_old_resource_version.py,sha256=S7ltVA-LrxUpQ8Q__AB0nQmezN8Mmnx5oKK62_baAKI,1500
34
- thds/mops/k8s/uncertain_future.py,sha256=60v9yVlhnCDN_yUv8l4Z4KafR4TsTGxN7dprkGI8pQQ,7152
34
+ thds/mops/k8s/uncertain_future.py,sha256=_ix-4EqZE_MY5sbLjT-lQ9GIa7woQ3iunPVtDapIhi8,7752
35
35
  thds/mops/k8s/wait_job.py,sha256=_X5lSn-3CE4V-_ra0kF1WtxkAiOgqSom8mU1-0hhMio,2445
36
36
  thds/mops/k8s/warn_image_backoff.py,sha256=ls_zLSnRbJjO4ICjq1Rk21EXh190l2dT6nKg-PT8Das,1934
37
- thds/mops/k8s/watch.py,sha256=4LBLZ9s9hp4jSOr3OTBDMWWCEDedgxsUoYt89zCLLsw,14020
37
+ thds/mops/k8s/watch.py,sha256=nH9HBoRCvt8FRhUav9at71QnTCh0plWga3rp7aUYb1E,14212
38
38
  thds/mops/k8s/tools/krsync.py,sha256=us7pXX0-bRMwD2oAno7Z6BJcPs6FgaUabHW0STyQJYg,1773
39
39
  thds/mops/k8s/tools/krsync.sh,sha256=fWgwkdzWnJeTbzEA_uBiIIi-bNU4nXAYj3dNovyRluU,747
40
40
  thds/mops/pure/__init__.py,sha256=3xLimQ2JWdeq1YgPs7bPwlwOspzPRwaR2w2KX7vfJU0,1624
@@ -109,8 +109,8 @@ thds/mops/pure/tools/summarize/cli.py,sha256=7kDtn24ok8oBO3jFjlMmOK3jnZYpMoE_5Y8
109
109
  thds/mops/pure/tools/summarize/run_summary.py,sha256=w45qiQr7elrHDiK9Hgs85gtU3gwLuXa447ih1Y23BBY,5776
110
110
  thds/mops/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
111
  thds/mops/testing/deferred_imports.py,sha256=f0ezCgQAtzTqW1yAOb0OWgsB9ZrlztLB894LtpWDaVw,3780
112
- thds_mops-3.9.20250929214325.dist-info/METADATA,sha256=eldgvZ4clfti48u5YIosxuUN1mcZZVz-UyQ55hlWezg,2225
113
- thds_mops-3.9.20250929214325.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
114
- thds_mops-3.9.20250929214325.dist-info/entry_points.txt,sha256=qKvCAaB80syXfxVR3xx6x9J0YJdaQWkIbVSw-NwFgMw,322
115
- thds_mops-3.9.20250929214325.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
116
- thds_mops-3.9.20250929214325.dist-info/RECORD,,
112
+ thds_mops-3.9.20250930235847.dist-info/METADATA,sha256=lZx15ZxrydRhLaSQMO7KnUyxnUoVzOAqBtgQSN1bgyY,2225
113
+ thds_mops-3.9.20250930235847.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
114
+ thds_mops-3.9.20250930235847.dist-info/entry_points.txt,sha256=qKvCAaB80syXfxVR3xx6x9J0YJdaQWkIbVSw-NwFgMw,322
115
+ thds_mops-3.9.20250930235847.dist-info/top_level.txt,sha256=LTZaE5SkWJwv9bwOlMbIhiS-JWQEEIcjVYnJrt-CriY,5
116
+ thds_mops-3.9.20250930235847.dist-info/RECORD,,