sovereign 1.0.0b123__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,5 +1,5 @@
1
1
  import asyncio
2
- from typing import Optional
2
+ from typing import Optional, final
3
3
  from multiprocessing import Process, cpu_count
4
4
  from contextlib import asynccontextmanager
5
5
 
@@ -21,7 +21,43 @@ from sovereign.context import NEW_CONTEXT
21
21
 
22
22
 
23
23
  ClientId = str
24
- 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()
25
61
  RENDER_SEMAPHORE = asyncio.Semaphore(cpu_count())
26
62
 
27
63
 
@@ -156,12 +192,17 @@ async def client_add(
156
192
  registration: RegisterClientRequest = Body(...),
157
193
  ):
158
194
  xds = registration.request
159
- if not cache.registered(xds):
160
- log.debug(f"Received registration for new client {xds}")
161
- ONDEMAND.put_nowait(await cache.register(xds))
162
- stats.increment("client.registration", tags=["status:registered"])
163
- return "Registering", 202
164
- else:
195
+ if cache.registered(xds):
165
196
  log.debug("Client already registered")
166
197
  stats.increment("client.registration", tags=["status:exists"])
167
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
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=sOVHVVv63I08_KIdaKR0hUnYLMXwoqh9jmsCw8albZE,5230
63
- sovereign-1.0.0b123.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
64
- sovereign-1.0.0b123.dist-info/METADATA,sha256=ZiGaNA-XWo08b2EghMAbSDPRhKeu3_s8ls2o-pXqJNU,6304
65
- sovereign-1.0.0b123.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
- sovereign-1.0.0b123.dist-info/entry_points.txt,sha256=VKJdnnN_HNL8xYQMXsFXfFmN6QkdXMEk5S964avxQJI,1404
67
- sovereign-1.0.0b123.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,,