sovereign 1.0.0b123.post2__py3-none-any.whl → 1.0.0b124__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 sovereign might be problematic. Click here for more details.

sovereign/cache.py CHANGED
@@ -19,11 +19,50 @@ CACHE: BaseCache
19
19
  CACHE_READ_TIMEOUT = config.cache_timeout
20
20
  WORKER_URL = "http://localhost:9080/client"
21
21
 
22
+
23
+ class DualCache(BaseCache):
24
+ """Cache that writes to both filesystem and Redis, reads filesystem first with Redis fallback"""
25
+
26
+ def __init__(
27
+ self, fs_cache: FileSystemCache, redis_cache: Optional[RedisCache] = None
28
+ ):
29
+ self.fs_cache = fs_cache
30
+ self.redis_cache = redis_cache
31
+
32
+ def get(self, key):
33
+ # Try filesystem first
34
+ if value := self.fs_cache.get(key):
35
+ stats.increment("cache.fs.hit")
36
+ return value
37
+
38
+ # Fallback to Redis if available
39
+ if self.redis_cache:
40
+ if value := self.redis_cache.get(key):
41
+ stats.increment("cache.redis.hit")
42
+ # Write back to filesystem
43
+ self.fs_cache.set(key, value)
44
+ return value
45
+
46
+ return None
47
+
48
+ def set(self, key, value, timeout=None):
49
+ self.fs_cache.set(key, value, timeout)
50
+ if self.redis_cache:
51
+ try:
52
+ self.redis_cache.set(key, value, timeout)
53
+ except Exception as e:
54
+ log.warning(f"Failed to write to Redis cache: {e}")
55
+
56
+
57
+ # Initialize caches
58
+ fs_cache = FileSystemCache(config.cache_path, default_timeout=0, hash_method=blake2s)
59
+ redis_cache = None
60
+
22
61
  redis = config.discovery_cache
23
62
  if redis.enabled:
24
63
  if mod := importlib.import_module("redis"):
25
64
  try:
26
- CACHE = RedisCache(
65
+ redis_cache = RedisCache(
27
66
  host=mod.Redis(
28
67
  host=redis.host,
29
68
  port=redis.port,
@@ -34,10 +73,11 @@ if redis.enabled:
34
73
  key_prefix="discovery_request_",
35
74
  default_timeout=redis.ttl,
36
75
  )
76
+ log.info("Redis cache enabled for dual caching")
37
77
  except Exception as e:
38
- log.exception(f"Tried to use redis for caching: {e}")
39
- else:
40
- CACHE = FileSystemCache(config.cache_path, default_timeout=0, hash_method=blake2s)
78
+ log.exception(f"Failed to initialize Redis cache: {e}")
79
+
80
+ CACHE = DualCache(fs_cache, redis_cache)
41
81
 
42
82
 
43
83
  class Entry(BaseModel):
@@ -71,8 +111,9 @@ async def lock():
71
111
 
72
112
  @stats.timed("cache.read_ms")
73
113
  async def blocking_read(
74
- req: DiscoveryRequest, timeout=CACHE_READ_TIMEOUT, poll_interval=0.05
114
+ req: DiscoveryRequest, timeout=CACHE_READ_TIMEOUT, poll_interval=0.5
75
115
  ) -> Optional[Entry]:
116
+ metric = "client.registration"
76
117
  id = client_id(req)
77
118
  if entry := read(id):
78
119
  return entry
@@ -80,16 +121,21 @@ async def blocking_read(
80
121
  registered = False
81
122
  registration = RegisterClientRequest(request=req)
82
123
  start = asyncio.get_event_loop().time()
124
+ attempt = 1
83
125
  while (asyncio.get_event_loop().time() - start) < timeout:
84
126
  if not registered:
85
127
  try:
86
128
  response = requests.put(WORKER_URL, json=registration.model_dump())
87
- if response.status_code == 200:
88
- registered = True
129
+ match response.status_code:
130
+ case 200 | 202:
131
+ registered = True
132
+ case 429:
133
+ stats.increment(metric, tags=["status:ratelimited"])
134
+ await asyncio.sleep(min(attempt, CACHE_READ_TIMEOUT))
135
+ attempt *= 2
89
136
  except Exception as e:
90
- stats.increment("client.registration", tags=["status:failed"])
137
+ stats.increment(metric, tags=["status:failed"])
91
138
  log.exception(f"Tried to register client but failed: {e}")
92
- await asyncio.sleep(1)
93
139
  if entry := read(id):
94
140
  return entry
95
141
  await asyncio.sleep(poll_interval)
sovereign/middlewares.py CHANGED
@@ -19,7 +19,7 @@ class RequestContextLogMiddleware(BaseHTTPMiddleware):
19
19
  response = await call_next(request)
20
20
  finally:
21
21
  req_id = get_request_id()
22
- response.headers["X-Request-ID"] = req_id
22
+ req_id = response.headers.setdefault("X-Request-Id", get_request_id())
23
23
  logs.access_logger.queue_log_fields(REQUEST_ID=req_id)
24
24
  _request_id_ctx_var.reset(token)
25
25
  return response
sovereign/worker.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
- import os
3
- from typing import Optional
2
+ from typing import Optional, final
4
3
  from multiprocessing import Process, cpu_count
5
4
  from contextlib import asynccontextmanager
6
5
 
@@ -22,7 +21,43 @@ from sovereign.context import NEW_CONTEXT
22
21
 
23
22
 
24
23
  ClientId = str
25
- ONDEMAND: asyncio.Queue[tuple[ClientId, DiscoveryRequest]] = asyncio.Queue(100)
24
+ OnDemandJob = tuple[ClientId, DiscoveryRequest]
25
+
26
+
27
+ @final
28
+ class RenderQueue:
29
+ def __init__(self, maxsize: int = 0):
30
+ self._queue: asyncio.Queue[OnDemandJob] = asyncio.Queue(maxsize)
31
+ self._set: set[ClientId] = set()
32
+ self._lock = asyncio.Lock()
33
+
34
+ async def put(self, item: OnDemandJob):
35
+ id_ = item[0]
36
+ async with self._lock:
37
+ if id_ not in self._set:
38
+ await self._queue.put(item)
39
+ self._set.add(id_)
40
+
41
+ def put_nowait(self, item: OnDemandJob):
42
+ id_ = item[0]
43
+ if id_ in self._set:
44
+ return
45
+ if self._queue.full():
46
+ raise asyncio.QueueFull
47
+ self._queue.put_nowait(item)
48
+ self._set.add(id_)
49
+
50
+ async def get(self):
51
+ item = await self._queue.get()
52
+ async with self._lock:
53
+ self._set.remove(item[0])
54
+ return item
55
+
56
+ def full(self):
57
+ return self._queue.full()
58
+
59
+
60
+ ONDEMAND = RenderQueue()
26
61
  RENDER_SEMAPHORE = asyncio.Semaphore(cpu_count())
27
62
 
28
63
 
@@ -61,26 +96,9 @@ if config.sources is not None:
61
96
  context_middleware.append(poller.add_to_context)
62
97
 
63
98
 
64
- def _render_process_entry(job: rendering.RenderJob): # runs in child
65
- """Entry point for render subprocess; increase niceness so it is lower priority.
66
-
67
- We deliberately raise the niceness (positive value) so these CPU bound template
68
- renders cannot starve the worker's main event loop handling incoming requests.
69
- """
70
- try:
71
- try:
72
- # Always lower scheduling priority by adding +2 niceness relative to parent
73
- os.nice(2) # best-effort; ignored if insufficient privileges
74
- except Exception:
75
- pass
76
- rendering.generate(job)
77
- except Exception:
78
- log.exception("Render process failed for %s", job.id)
79
-
80
-
81
99
  def render(job: rendering.RenderJob):
82
- log.debug(f"Spawning render process for {job.id} with lowered priority")
83
- process = Process(target=_render_process_entry, args=(job,), daemon=True)
100
+ log.debug(f"Spawning render process for {job.id}")
101
+ process = Process(target=rendering.generate, args=[job])
84
102
  process.start()
85
103
  return process
86
104
 
@@ -174,12 +192,17 @@ async def client_add(
174
192
  registration: RegisterClientRequest = Body(...),
175
193
  ):
176
194
  xds = registration.request
177
- if not cache.registered(xds):
178
- log.debug(f"Received registration for new client {xds}")
179
- ONDEMAND.put_nowait(await cache.register(xds))
180
- stats.increment("client.registration", tags=["status:registered"])
181
- return "Registering", 202
182
- else:
195
+ if cache.registered(xds):
183
196
  log.debug("Client already registered")
184
197
  stats.increment("client.registration", tags=["status:exists"])
185
198
  return "Registered", 200
199
+ else:
200
+ log.debug(f"Received registration for new client {xds}")
201
+ id, req = await cache.register(xds)
202
+ try:
203
+ ONDEMAND.put_nowait((id, req))
204
+ except asyncio.QueueFull:
205
+ stats.increment("client.registration", tags=["status:queue_full"])
206
+ return "Slow down :(", 429
207
+ stats.increment("client.registration", tags=["status:registered"])
208
+ return "Registering", 202
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 1.0.0b123.post2
3
+ Version: 1.0.0b124
4
4
  Summary: Envoy Proxy control-plane written in Python
5
5
  Home-page: https://pypi.org/project/sovereign/
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  sovereign/__init__.py,sha256=m8MVzaMSW4AvAqHvUAsXFdp8Oas5oQ8X7BcVt7Hfcik,1431
2
2
  sovereign/app.py,sha256=fsf4Jgni2G4EYJO0oQSWfGRZVDBvE2Yfick4n2YR6K4,4876
3
- sovereign/cache.py,sha256=P2LAskGxZSFgQ392aKz7auHty57HJNepQ7xl2-iWbBA,3842
3
+ sovereign/cache.py,sha256=f7d-x5ksRmd_tbSgdl5xKFXmwgUsMWECg98S5DZYc3w,5333
4
4
  sovereign/constants.py,sha256=qdWD1lTvkaW5JGF7TmZhfksQHlRAJFVqbG7v6JQA9k8,46
5
5
  sovereign/context.py,sha256=aoGJ5k1n8ytCk7jggQ6XTx6Hx_ocy7cEkvGOFS3gzzc,8912
6
6
  sovereign/dynamic_config/__init__.py,sha256=0hrI9Y-FzDywEM9Lu6i2mPFhs1c47C096R1B_-E3sKA,3161
@@ -12,7 +12,7 @@ sovereign/logging/application_logger.py,sha256=HjrGTi2zZ06AaToDVdSv4MNIF6aWN6vFW
12
12
  sovereign/logging/base_logger.py,sha256=ScOzHs8Rt1RZaUZGvaJSAlDEjD0BxkD5sLKSm2GgM0I,1243
13
13
  sovereign/logging/bootstrapper.py,sha256=gWFzIVsfeMdv7-d2Z6Fiw7J0xcuZzc4z2F4Iqn1KG30,1296
14
14
  sovereign/logging/types.py,sha256=rGqJAEVvgvzHy4aPfvEH6yQ-yblXNkEcWG7G8l9ALEA,282
15
- sovereign/middlewares.py,sha256=tQazHAtIdUc1hWhopg33x83-g-JcilU4HdjzoxFe6NU,3053
15
+ sovereign/middlewares.py,sha256=6w4JpvtNGvQA4rocQsYQjuu-ckhpKT6gKYA16T-kiqA,3082
16
16
  sovereign/modifiers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  sovereign/modifiers/lib.py,sha256=Cx0VrpTKbSjb3YmHyG4Jy6YEaPlrwpeqNaom3zu1_hw,2885
18
18
  sovereign/rendering.py,sha256=MIA7se7-C4WTWf7xZSgqpf7NvhDT7NkZbR3_G9N1dHI,5015
@@ -59,9 +59,9 @@ sovereign/views/crypto.py,sha256=7y0eHWtt-bbr2CwHEkH7odPaJ1IEviU-71U-MYJD0Kc,336
59
59
  sovereign/views/discovery.py,sha256=B_D1ckfbN1dSKBvuFCTyfB79GUUriCADTB53OwZ8D4Q,2409
60
60
  sovereign/views/healthchecks.py,sha256=TaXbxkX679jyQ8v5FxtBa2Qa0Z7KuqQ10WgAqfuVGUc,1743
61
61
  sovereign/views/interface.py,sha256=FmQ7LiUPLSvkEDOKCncrnKMD9g1lJKu-DQNbbyi8mqk,6346
62
- sovereign/worker.py,sha256=JyDD6Ei0qfUZvBDG_d0pwftjO8lnOecnt1-VZihWCy4,5955
63
- sovereign-1.0.0b123.post2.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
64
- sovereign-1.0.0b123.post2.dist-info/METADATA,sha256=xOrKDrcRoa-og210CfD5xqxESzxEbY20u-Ig9X-Byfw,6310
65
- sovereign-1.0.0b123.post2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
- sovereign-1.0.0b123.post2.dist-info/entry_points.txt,sha256=VKJdnnN_HNL8xYQMXsFXfFmN6QkdXMEk5S964avxQJI,1404
67
- sovereign-1.0.0b123.post2.dist-info/RECORD,,
62
+ sovereign/worker.py,sha256=JWyZEsD-unguTWOMIwBWbWOTBppOozdbj_O4TrvN2BE,6315
63
+ sovereign-1.0.0b124.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
64
+ sovereign-1.0.0b124.dist-info/METADATA,sha256=zG_mBD_7-89gp9buqzGEKxAr3iv6fTEgjOFXw44hjRs,6304
65
+ sovereign-1.0.0b124.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
+ sovereign-1.0.0b124.dist-info/entry_points.txt,sha256=VKJdnnN_HNL8xYQMXsFXfFmN6QkdXMEk5S964avxQJI,1404
67
+ sovereign-1.0.0b124.dist-info/RECORD,,