sovereign 1.0.0b101__tar.gz → 1.0.0b103__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.

Potentially problematic release.


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

Files changed (66) hide show
  1. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/PKG-INFO +1 -1
  2. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/pyproject.toml +1 -1
  3. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/context.py +1 -0
  4. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/schemas.py +3 -2
  5. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/sources/poller.py +198 -6
  6. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/templates/resources.html +7 -1
  7. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/views/api.py +24 -1
  8. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/views/interface.py +2 -16
  9. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/worker.py +23 -37
  10. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/LICENSE.txt +0 -0
  11. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/README.md +0 -0
  12. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/__init__.py +0 -0
  13. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/app.py +0 -0
  14. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/cache.py +0 -0
  15. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/constants.py +0 -0
  16. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/dynamic_config/__init__.py +0 -0
  17. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/dynamic_config/deser.py +0 -0
  18. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/dynamic_config/loaders.py +0 -0
  19. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/error_info.py +0 -0
  20. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/logging/access_logger.py +0 -0
  21. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/logging/application_logger.py +0 -0
  22. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/logging/base_logger.py +0 -0
  23. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/logging/bootstrapper.py +0 -0
  24. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/logging/types.py +0 -0
  25. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/middlewares.py +0 -0
  26. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/modifiers/__init__.py +0 -0
  27. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/modifiers/lib.py +0 -0
  28. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/rendering.py +0 -0
  29. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/response_class.py +0 -0
  30. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/server.py +0 -0
  31. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/sources/__init__.py +0 -0
  32. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/sources/file.py +0 -0
  33. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/sources/inline.py +0 -0
  34. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/sources/lib.py +0 -0
  35. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/static/node_expression.js +0 -0
  36. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/static/panel.js +0 -0
  37. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/static/sass/style.scss +0 -0
  38. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/static/style.css +0 -0
  39. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/statistics.py +0 -0
  40. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/templates/base.html +0 -0
  41. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/templates/err.html +0 -0
  42. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/testing/loaders.py +0 -0
  43. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/testing/modifiers.py +0 -0
  44. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/tracing.py +0 -0
  45. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/__init__.py +0 -0
  46. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/auth.py +0 -0
  47. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/crypto/__init__.py +0 -0
  48. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/crypto/crypto.py +0 -0
  49. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/crypto/suites/__init__.py +0 -0
  50. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/crypto/suites/aes_gcm_cipher.py +0 -0
  51. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/crypto/suites/base_cipher.py +0 -0
  52. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/crypto/suites/disabled_cipher.py +0 -0
  53. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/crypto/suites/fernet_cipher.py +0 -0
  54. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/dictupdate.py +0 -0
  55. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/eds.py +0 -0
  56. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/entry_point_loader.py +0 -0
  57. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/mock.py +0 -0
  58. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/resources.py +0 -0
  59. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/templates.py +0 -0
  60. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/timer.py +0 -0
  61. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/version_info.py +0 -0
  62. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/utils/weighted_clusters.py +0 -0
  63. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/views/__init__.py +0 -0
  64. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/views/crypto.py +0 -0
  65. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/views/discovery.py +0 -0
  66. {sovereign-1.0.0b101 → sovereign-1.0.0b103}/src/sovereign/views/healthchecks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 1.0.0b101
3
+ Version: 1.0.0b103
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
  [tool.poetry]
2
2
  name = "sovereign"
3
- version = "1.0.0b101"
3
+ version = "1.0.0b103"
4
4
  description = "Envoy Proxy control-plane written in Python"
5
5
  license = "Apache-2.0"
6
6
  packages = [
@@ -75,6 +75,7 @@ class TemplateContext:
75
75
  new = hash(result)
76
76
 
77
77
  if old != new:
78
+ stats.increment("context.updated", tags=[f"context:{name}"])
78
79
  self.hashes[name] = new
79
80
  # Debounced event notification to the worker
80
81
  if self.notify_consumers:
@@ -44,7 +44,7 @@ class CacheStrategy(str, Enum):
44
44
 
45
45
 
46
46
  class SourceData(BaseModel):
47
- scopes: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
47
+ scopes: Dict[str, List[Dict[str, Any]]] = Field(default_factory=dict)
48
48
 
49
49
 
50
50
  class ConfiguredSource(BaseModel):
@@ -410,7 +410,7 @@ class DiscoveryRequest(BaseModel):
410
410
  return f"version={self.envoy_version}, cluster={self.node.cluster}, resource={self.resource_type}, names={self.resources}"
411
411
 
412
412
  def __str__(self) -> str:
413
- return f"DiscoverRequest({self.debug()})"
413
+ return f"DiscoveryRequest({self.debug()})"
414
414
 
415
415
 
416
416
  class DiscoveryResponse(BaseModel):
@@ -645,6 +645,7 @@ class AccessLogConfiguration(BaseSettings):
645
645
  class LoggingConfiguration(BaseSettings):
646
646
  application_logs: ApplicationLogConfiguration = ApplicationLogConfiguration()
647
647
  access_logs: AccessLogConfiguration = AccessLogConfiguration()
648
+ log_source_diffs: bool = False
648
649
 
649
650
 
650
651
  class ContextFileCache(BaseSettings):
@@ -1,3 +1,5 @@
1
+ import json
2
+ import uuid
1
3
  import asyncio
2
4
  import traceback
3
5
  from copy import deepcopy
@@ -7,11 +9,7 @@ from typing import Iterable, Any, Dict, List, Union, Type, Optional
7
9
 
8
10
  from glom import glom, PathAccessError
9
11
 
10
- from sovereign.schemas import (
11
- ConfiguredSource,
12
- SourceData,
13
- Node,
14
- )
12
+ from sovereign.schemas import ConfiguredSource, SourceData, Node, config
15
13
  from sovereign.utils.entry_point_loader import EntryPointLoader
16
14
  from sovereign.sources.lib import Source
17
15
  from sovereign.modifiers.lib import Modifier, GlobalModifier
@@ -36,6 +34,179 @@ Mods = Dict[str, Type[Modifier]]
36
34
  GMods = Dict[str, Type[GlobalModifier]]
37
35
 
38
36
 
37
+ def _deep_diff(old, new, path="") -> list[dict[str, Any]]:
38
+ changes = []
39
+
40
+ # handle add/remove
41
+ if (old, new) == (None, None):
42
+ return changes
43
+ elif old is None:
44
+ changes.append({
45
+ "op": "add",
46
+ "path": path,
47
+ "value": new
48
+ })
49
+ return changes
50
+ elif new is None:
51
+ changes.append({
52
+ "op": "remove",
53
+ "path": path,
54
+ "old_value": old
55
+ })
56
+ return changes
57
+
58
+ # handle completely different types
59
+ if type(old) is not type(new):
60
+ changes.append({
61
+ "op": "change",
62
+ "path": path,
63
+ "old_value": old,
64
+ "new_value": new
65
+ })
66
+ return changes
67
+
68
+ # handle fields recursively
69
+ if isinstance(old, dict) and isinstance(new, dict):
70
+ all_keys = set(old.keys()) | set(new.keys())
71
+
72
+ for key in sorted(all_keys):
73
+ old_val = old.get(key)
74
+ new_val = new.get(key)
75
+
76
+ current_path = f"{path}.{key}" if path else key
77
+
78
+ if key not in old:
79
+ changes.append({
80
+ "op": "add",
81
+ "path": current_path,
82
+ "value": new_val
83
+ })
84
+ elif key not in new:
85
+ changes.append({
86
+ "op": "remove",
87
+ "path": current_path,
88
+ "old_value": old_val
89
+ })
90
+ elif old_val != new_val:
91
+ nested_changes = _deep_diff(old_val, new_val, current_path)
92
+ changes.extend(nested_changes)
93
+
94
+ # handle items recursively
95
+ elif isinstance(old, list) and isinstance(new, list):
96
+ max_len = max(len(old), len(new))
97
+
98
+ for i in range(max_len):
99
+ current_path = f"{path}[{i}]" if path else f"[{i}]"
100
+
101
+ if i >= len(old):
102
+ changes.append({
103
+ "op": "add",
104
+ "path": current_path,
105
+ "value": new[i]
106
+ })
107
+ elif i >= len(new):
108
+ changes.append({
109
+ "op": "remove",
110
+ "path": current_path,
111
+ "old_value": old[i]
112
+ })
113
+ elif old[i] != new[i]:
114
+ nested_changes = _deep_diff(old[i], new[i], current_path)
115
+ changes.extend(nested_changes)
116
+
117
+ # handle primitives
118
+ else:
119
+ if old != new:
120
+ changes.append({
121
+ "op": "change",
122
+ "path": path,
123
+ "old_value": old,
124
+ "new_value": new
125
+ })
126
+
127
+ return changes
128
+
129
+
130
+ def per_field_diff(old, new) -> list[dict[str, Any]]:
131
+ changes = []
132
+ max_len = max(len(old), len(new))
133
+
134
+ for i in range(max_len):
135
+ old_inst = old[i] if i < len(old) else None
136
+ new_inst = new[i] if i < len(new) else None
137
+
138
+ if old_inst is None:
139
+ changes.append({
140
+ "op": "add",
141
+ "path": f"[{i}]",
142
+ "value": new_inst
143
+ })
144
+ elif new_inst is None:
145
+ changes.append({
146
+ "op": "remove",
147
+ "path": f"[{i}]",
148
+ "old_value": old_inst
149
+ })
150
+ elif old_inst != new_inst:
151
+ # Use the deep diff with index prefix
152
+ field_changes = _deep_diff(old_inst, new_inst, f"[{i}]")
153
+ changes.extend(field_changes)
154
+
155
+ return changes
156
+
157
+
158
+ def _gen_uuid(diff_summary: dict[str, Any]) -> str:
159
+ blob = json.dumps(diff_summary, sort_keys=True, separators=('', ''))
160
+ return str(uuid.uuid5(uuid.NAMESPACE_DNS, blob))
161
+
162
+
163
+ def source_diff_summary(prev, curr) -> dict[str, Any]:
164
+ if prev is None:
165
+ summary = {
166
+ "type": "initial_load",
167
+ "scopes": {
168
+ scope: {"added": len(instances)}
169
+ for scope, instances in curr.scopes.items()
170
+ if instances
171
+ }
172
+ }
173
+ else:
174
+ summary = {
175
+ "type": "update",
176
+ "scopes": {}
177
+ }
178
+
179
+ all_scopes = set(prev.scopes.keys()) | set(curr.scopes.keys())
180
+
181
+ for scope in sorted(all_scopes):
182
+ old = prev.scopes.get(scope, [])
183
+ new = curr.scopes.get(scope, [])
184
+
185
+ n_old = len(old)
186
+ n_new = len(new)
187
+
188
+ scope_changes = {}
189
+
190
+ if n_old == 0 and n_new > 0:
191
+ scope_changes["added"] = n_new
192
+ elif n_old > 0 and n_new == 0:
193
+ scope_changes["removed"] = n_old
194
+ elif old != new:
195
+ detailed_changes = per_field_diff(old, new)
196
+ if detailed_changes:
197
+ scope_changes["field_changes"] = detailed_changes
198
+ scope_changes["count_change"] = n_new - n_old
199
+
200
+ if scope_changes:
201
+ summary["scopes"][scope] = scope_changes
202
+
203
+ if not summary["scopes"]:
204
+ summary = {"type": "no_changes"}
205
+
206
+ summary["uuid"] = _gen_uuid(summary)
207
+ return summary
208
+
209
+
39
210
  class SourcePoller:
40
211
  def __init__(
41
212
  self,
@@ -160,7 +331,10 @@ class SourcePoller:
160
331
  try:
161
332
  new = SourceData()
162
333
  for source in self.sources:
163
- new.scopes[source.scope].extend(source.get())
334
+ scope = source.scope
335
+ if scope not in new.scopes:
336
+ new.scopes[scope] = []
337
+ new.scopes[scope].extend(source.get())
164
338
  except Exception as e:
165
339
  self.logger.error(
166
340
  event="Error while refreshing sources",
@@ -179,9 +353,25 @@ class SourcePoller:
179
353
  else:
180
354
  self.stats.increment("sources.refreshed")
181
355
  self.last_updated = datetime.now()
356
+ old_data = getattr(self, "source_data", None)
182
357
  self.instance_count = len(
183
358
  [instance for scope in new.scopes.values() for instance in scope]
184
359
  )
360
+
361
+ if config.logging.log_source_diffs:
362
+ diff_summary = source_diff_summary(old_data, new)
363
+ # printing json directly because the logger is fucking stupid
364
+ print(
365
+ json.dumps(
366
+ dict(
367
+ event="Sources refreshed with changes",
368
+ level="info",
369
+ diff=diff_summary,
370
+ total_instances=self.instance_count,
371
+ )
372
+ )
373
+ )
374
+
185
375
  self.source_data = new
186
376
  return True
187
377
 
@@ -260,6 +450,8 @@ class SourcePoller:
260
450
  or is_debug_request(node_value)
261
451
  )
262
452
  if match:
453
+ if scope not in ret.scopes:
454
+ ret.scopes[scope] = []
263
455
  ret.scopes[scope].append(instance)
264
456
  return ret
265
457
 
@@ -14,6 +14,12 @@
14
14
  ];
15
15
  const resources = resourceNames.map((name, index) => ({ name: name, index: index }));
16
16
  let filteredResources = [...resources];
17
+
18
+ // Function to set envoy_version cookie and reload page
19
+ function setEnvoyVersion(version) {
20
+ document.cookie = `envoy_version=${version}; path=/ui/resources/; max-age=31536000`;
21
+ window.location.reload();
22
+ }
17
23
  </script>
18
24
  <style>
19
25
  #filterInput {
@@ -284,7 +290,7 @@
284
290
  </div>
285
291
  {% for v in available_versions %}
286
292
  <a class="dropdown-item{% if v == version %} is-active{% endif %}"
287
- href="/ui/set-version?version={{ v }}">
293
+ href="#" onclick="setEnvoyVersion('{{ v }}'); return false;">
288
294
  {{ v.replace('_', '') }}
289
295
  </a>
290
296
  {% endfor %}
@@ -11,6 +11,20 @@ from sovereign.utils.mock import mock_discovery_request
11
11
  router = APIRouter()
12
12
 
13
13
 
14
+ def _traverse(data, prefix, expressions):
15
+ for key, value in data.items():
16
+ path = f"{prefix}.{key}" if prefix else key
17
+ if isinstance(value, dict):
18
+ yield from _traverse(value, path, expressions)
19
+ else:
20
+ yield f"{path}={value}"
21
+
22
+
23
+ def expand_metadata_to_expr(m):
24
+ exprs = []
25
+ yield from _traverse(m, "", exprs)
26
+
27
+
14
28
  @router.get("/resources/{resource_type}", summary="Get resources for a given type")
15
29
  async def resource(
16
30
  resource_type: DiscoveryTypes = Path(title="xDS Resource type"),
@@ -19,16 +33,25 @@ async def resource(
19
33
  service_cluster: Optional[str] = Query("*", title="Envoy Service cluster"),
20
34
  region: Optional[str] = Query(None, title="Locality Zone"),
21
35
  version: Optional[str] = Query(None, title="Envoy Semantic Version"),
36
+ metadata: Optional[str] = Query(None, title="Envoy node metadata to filter by"),
22
37
  ) -> Response:
38
+ expressions = [f"cluster={service_cluster}"]
39
+ try:
40
+ metadata = json.loads(metadata or "{}")
41
+ for expr in expand_metadata_to_expr(metadata):
42
+ expressions.append(expr)
43
+ except Exception:
44
+ pass
23
45
  kwargs = dict(
24
46
  api_version=api_version,
25
47
  resource_type=DiscoveryTypes(resource_type).value,
26
48
  resource_names=resource_name,
27
49
  version=version,
28
50
  region=region,
29
- expressions=[f"cluster={service_cluster}"],
51
+ expressions=expressions,
30
52
  )
31
53
  req = mock_discovery_request(**{k: v for k, v in kwargs.items() if v is not None})
54
+ print(req)
32
55
  response = await cache.blocking_read(req)
33
56
  if content := getattr(response, "text", None):
34
57
  return Response(content, media_type="application/json")
@@ -5,7 +5,7 @@ from typing import Any, Dict, List
5
5
  from fastapi import APIRouter, Cookie, Path, Query
6
6
  from fastapi.encoders import jsonable_encoder
7
7
  from fastapi.requests import Request
8
- from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
8
+ from fastapi.responses import HTMLResponse, JSONResponse, Response
9
9
 
10
10
  from sovereign import html_templates, cache
11
11
  from sovereign.schemas import DiscoveryTypes, XDS_TEMPLATES
@@ -18,6 +18,7 @@ all_types = [t.value for t in DiscoveryTypes]
18
18
 
19
19
 
20
20
  @router.get("/")
21
+ @router.get("/resources")
21
22
  async def ui_main(request: Request) -> HTMLResponse:
22
23
  try:
23
24
  return html_templates.TemplateResponse(
@@ -42,21 +43,6 @@ async def ui_main(request: Request) -> HTMLResponse:
42
43
  )
43
44
 
44
45
 
45
- @router.get(
46
- "/set-version", summary="Filter the UI by a certain Envoy Version (stores a Cookie)"
47
- )
48
- async def set_envoy_version(
49
- request: Request,
50
- version: str = Query(
51
- "__any__", title="The clients envoy version to emulate in this XDS request"
52
- ),
53
- ) -> Response:
54
- url = request.headers.get("Referer", "/ui")
55
- response = RedirectResponse(url=url)
56
- response.set_cookie(key="envoy_version", value=version)
57
- return response
58
-
59
-
60
46
  @router.get(
61
47
  "/resources/{xds_type}", summary="List available resources for a given xDS type"
62
48
  )
@@ -1,8 +1,7 @@
1
1
  import asyncio
2
2
  from typing import Optional
3
- from multiprocessing import Process
3
+ from multiprocessing import Process, cpu_count
4
4
  from contextlib import asynccontextmanager
5
- from concurrent.futures import ThreadPoolExecutor
6
5
 
7
6
  from fastapi import FastAPI, Body
8
7
 
@@ -23,10 +22,9 @@ from sovereign.context import NEW_CONTEXT
23
22
 
24
23
  ClientId = str
25
24
  ONDEMAND: asyncio.Queue[tuple[ClientId, DiscoveryRequest]] = asyncio.Queue(100)
26
- executor = ThreadPoolExecutor(4)
25
+ RENDER_SEMAPHORE = asyncio.Semaphore(cpu_count())
27
26
 
28
27
 
29
- # TODO: do something about this ---------------------------------------
30
28
  def hidden_field(*args, **kwargs):
31
29
  return "(value hidden)"
32
30
 
@@ -62,43 +60,37 @@ if config.sources is not None:
62
60
  context_middleware.append(poller.add_to_context)
63
61
 
64
62
 
65
- if poller is not None:
66
- poller.lazy_load_modifiers(config.modifiers)
67
- poller.lazy_load_global_modifiers(config.global_modifiers)
68
-
69
- template_context.middleware = context_middleware
70
- # ---------------------------------------------------------------------
71
-
72
-
73
63
  def render(job: rendering.RenderJob):
74
64
  log.debug(f"Spawning render process for {job.id}")
75
65
  Process(target=rendering.generate, args=[job]).start()
76
66
 
77
67
 
78
- def batch_render(jobs: list[rendering.RenderJob]):
79
- log.debug("Spawning batch render process for all clients")
80
- Process(target=rendering.batch_generate, args=[jobs]).start()
68
+ async def submit_render(job: rendering.RenderJob):
69
+ async with RENDER_SEMAPHORE:
70
+ render(job)
81
71
 
82
72
 
83
73
  async def render_on_event():
84
74
  while True:
85
75
  # block forever until new context arrives
86
76
  await NEW_CONTEXT.wait()
87
- stats.increment("template.render_on_event")
88
77
  log.debug("New context detected, re-rendering templates")
89
78
  try:
90
79
  if registered := cache.clients():
91
80
  log.debug("New context detected, re-rendering templates")
92
- batch_render(
93
- [
94
- rendering.RenderJob(
95
- id=client,
96
- request=request,
97
- context=template_context.get_context(request),
98
- )
99
- for client, request in registered
100
- ]
101
- )
81
+ jobs = [
82
+ rendering.RenderJob(
83
+ id=client,
84
+ request=request,
85
+ context=template_context.get_context(request),
86
+ )
87
+ for client, request in registered
88
+ ]
89
+ tasks = [submit_render(job) for job in jobs]
90
+ size = len(tasks)
91
+ stats.increment("template.render_on_event", tags=[f"batch_size:{size}"])
92
+ await asyncio.gather(*tasks)
93
+ log.debug(f"Completed rendering {size} jobs")
102
94
  finally:
103
95
  NEW_CONTEXT.clear()
104
96
 
@@ -111,7 +103,7 @@ async def render_on_demand():
111
103
  job = rendering.RenderJob(
112
104
  id=id, request=request, context=template_context.get_context(request)
113
105
  )
114
- await asyncio.get_event_loop().run_in_executor(executor, render, job)
106
+ await submit_render(job)
115
107
  ONDEMAND.task_done()
116
108
 
117
109
 
@@ -124,25 +116,19 @@ async def lifespan(_: FastAPI):
124
116
 
125
117
  # Template context
126
118
  log.debug("Starting context loop")
119
+ template_context.middleware = context_middleware
127
120
  asyncio.create_task(template_context.start())
128
121
  await NEW_CONTEXT.wait() # first refresh finished
129
122
 
130
123
  # Source polling
131
124
  if poller is not None:
132
125
  log.debug("Starting source poller")
133
- import threading
134
-
135
- threading.Thread(target=poller_thread, args=[poller], daemon=True).start()
126
+ poller.lazy_load_modifiers(config.modifiers)
127
+ poller.lazy_load_global_modifiers(config.global_modifiers)
128
+ asyncio.create_task(poller.poll_forever())
136
129
  yield
137
130
 
138
131
 
139
- def poller_thread(poller):
140
- log.debug("Starting source poller")
141
- loop = asyncio.new_event_loop()
142
- asyncio.set_event_loop(loop)
143
- loop.run_until_complete(poller.poll_forever())
144
-
145
-
146
132
  worker = FastAPI(lifespan=lifespan)
147
133
  try:
148
134
  import sentry_sdk
File without changes
File without changes