sovereign 1.0.0b123__py3-none-any.whl → 1.0.0b134__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.
Files changed (38) hide show
  1. sovereign/app.py +1 -1
  2. sovereign/cache/__init__.py +182 -0
  3. sovereign/cache/backends/__init__.py +110 -0
  4. sovereign/cache/backends/s3.py +139 -0
  5. sovereign/cache/filesystem.py +42 -0
  6. sovereign/cache/types.py +15 -0
  7. sovereign/context.py +20 -18
  8. sovereign/events.py +49 -0
  9. sovereign/middlewares.py +1 -1
  10. sovereign/rendering.py +74 -35
  11. sovereign/schemas.py +112 -110
  12. sovereign/server.py +4 -3
  13. sovereign/sources/poller.py +20 -4
  14. sovereign/statistics.py +1 -1
  15. sovereign/templates/base.html +59 -46
  16. sovereign/templates/resources.html +40 -835
  17. sovereign/utils/mock.py +7 -3
  18. sovereign/views/healthchecks.py +1 -1
  19. sovereign/views/interface.py +34 -15
  20. sovereign/worker.py +87 -46
  21. {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/METADATA +4 -5
  22. {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/RECORD +33 -24
  23. {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/WHEEL +1 -1
  24. {sovereign-1.0.0b123.dist-info → sovereign-1.0.0b134.dist-info}/entry_points.txt +3 -0
  25. sovereign_files/__init__.py +0 -0
  26. sovereign_files/static/darkmode.js +51 -0
  27. sovereign_files/static/node_expression.js +42 -0
  28. sovereign_files/static/resources.css +246 -0
  29. sovereign_files/static/resources.js +642 -0
  30. sovereign_files/static/sass/style.scss +33 -0
  31. sovereign_files/static/style.css +16143 -0
  32. sovereign_files/static/style.css.map +1 -0
  33. sovereign/cache.py +0 -133
  34. sovereign/static/node_expression.js +0 -16
  35. sovereign/static/sass/style.scss +0 -27
  36. sovereign/static/style.css +0 -13553
  37. sovereign-1.0.0b123.dist-info/LICENSE.txt +0 -13
  38. {sovereign → sovereign_files}/static/panel.js +0 -0
sovereign/rendering.py CHANGED
@@ -7,7 +7,11 @@ Functions used to render and return discovery responses to Envoy proxies.
7
7
  The templates are configurable. `todo See ref:Configuration#Templates`
8
8
  """
9
9
 
10
- from typing import Any, Dict, List
10
+ import traceback
11
+ from concurrent.futures import ThreadPoolExecutor
12
+ from multiprocessing import Process, Pipe, cpu_count
13
+ from multiprocessing.connection import Connection
14
+ from typing import Any
11
15
 
12
16
  import yaml
13
17
  import pydantic
@@ -21,12 +25,14 @@ try:
21
25
  except ImportError:
22
26
  SENTRY_INSTALLED = False
23
27
 
24
- from sovereign import config, logs, cache, stats
28
+ from sovereign import config, logs, cache, stats, application_logger as log
25
29
  from sovereign.schemas import (
26
30
  DiscoveryRequest,
27
31
  ProcessedTemplate,
28
32
  )
29
33
 
34
+ # limit render jobs to number of cores
35
+ POOL = ThreadPoolExecutor(max_workers=cpu_count())
30
36
 
31
37
  type_urls = {
32
38
  "v2": {
@@ -54,42 +60,75 @@ class RenderJob(pydantic.BaseModel):
54
60
  request: DiscoveryRequest
55
61
  context: dict[str, Any]
56
62
 
63
+ def submit(self):
64
+ _ = POOL.submit(self._run)
65
+
66
+ def _run(self):
67
+ rx, tx = Pipe()
68
+ proc = Process(target=generate, args=[self, tx])
69
+ proc.start()
70
+ log.debug(
71
+ (
72
+ f"Spawning process for id={self.id} "
73
+ f"max_workers={POOL._max_workers} "
74
+ f"threads={len(POOL._threads)} "
75
+ f"shutdown={POOL._shutdown} "
76
+ f"queue_size={POOL._work_queue.qsize()}"
77
+ )
78
+ )
79
+ proc.join(timeout=30) # TODO: render timeout configurable
80
+ while rx.poll(timeout=30):
81
+ level, message = rx.recv()
82
+ logger = getattr(log, level)
83
+ logger(message)
84
+
57
85
 
58
- def generate(job: RenderJob) -> None:
86
+ def generate(job: RenderJob, tx: Connection) -> None:
59
87
  request = job.request
60
88
  tags = [f"type:{request.resource_type}"]
61
- stats.increment("template.render", tags=tags)
62
- with stats.timed("template.render_ms", tags=tags):
63
- content = request.template(
64
- discovery_request=request,
65
- host_header=request.desired_controlplane,
66
- resource_names=request.resources,
67
- **job.context,
68
- )
69
- if not request.template.is_python_source:
70
- assert isinstance(content, str)
71
- content = deserialize_config(content)
72
- assert isinstance(content, dict)
73
- resources = filter_resources(content["resources"], request.resources)
74
- add_type_urls(request.api_version, request.resource_type, resources)
75
- response = ProcessedTemplate(resources=resources)
76
- cache.write(
77
- job.id,
78
- cache.Entry(
79
- text=response.model_dump_json(indent=None),
80
- len=len(response.resources),
81
- version=response.version_info,
82
- node=request.node,
83
- ),
89
+ try:
90
+ with stats.timed("template.render_ms", tags=tags):
91
+ content = request.template(
92
+ discovery_request=request,
93
+ host_header=request.desired_controlplane,
94
+ resource_names=request.resources,
95
+ **job.context,
96
+ )
97
+ if not request.template.is_python_source:
98
+ assert isinstance(content, str)
99
+ content = deserialize_config(content)
100
+ assert isinstance(content, dict)
101
+ resources = filter_resources(content["resources"], request.resources)
102
+ add_type_urls(request.api_version, request.resource_type, resources)
103
+ response = ProcessedTemplate(resources=resources)
104
+ cache.write(
105
+ job.id,
106
+ cache.Entry(
107
+ text=response.model_dump_json(indent=None),
108
+ len=len(response.resources),
109
+ version=response.version_info,
110
+ node=request.node,
111
+ ),
112
+ )
113
+ tags.append("result:ok")
114
+ tx.send(("debug", f"Completed rendering of {request} for {job.id}"))
115
+ except Exception as e:
116
+ tx.send(
117
+ (
118
+ "error",
119
+ f"Failed to render job for {job.id}: " + str(traceback.format_exc()),
120
+ )
84
121
  )
122
+ tags.append("result:err")
123
+ tags.append(f"error:{e.__class__.__name__.lower()}")
124
+ if SENTRY_INSTALLED and config.sentry_dsn:
125
+ sentry_sdk.capture_exception(e)
126
+ finally:
127
+ stats.increment("template.render", tags=tags)
128
+ tx.close()
85
129
 
86
130
 
87
- def batch_generate(jobs: list[RenderJob]) -> None:
88
- for job in jobs:
89
- generate(job)
90
-
91
-
92
- def deserialize_config(content: str) -> Dict[str, Any]:
131
+ def deserialize_config(content: str) -> dict[str, Any]:
93
132
  try:
94
133
  envoy_configuration = yaml.safe_load(content)
95
134
  except (ParserError, ScannerError) as e:
@@ -119,8 +158,8 @@ def deserialize_config(content: str) -> Dict[str, Any]:
119
158
 
120
159
 
121
160
  def filter_resources(
122
- generated: List[Dict[str, Any]], requested: List[str]
123
- ) -> List[Dict[str, Any]]:
161
+ generated: list[dict[str, Any]], requested: list[str]
162
+ ) -> list[dict[str, Any]]:
124
163
  """
125
164
  If Envoy specifically requested a resource, this removes everything
126
165
  that does not match the name of the resource.
@@ -131,7 +170,7 @@ def filter_resources(
131
170
  return [resource for resource in generated if resource_name(resource) in requested]
132
171
 
133
172
 
134
- def resource_name(resource: Dict[str, Any]) -> str:
173
+ def resource_name(resource: dict[str, Any]) -> str:
135
174
  name = resource.get("name") or resource.get("cluster_name")
136
175
  if isinstance(name, str):
137
176
  return name
sovereign/schemas.py CHANGED
@@ -1,6 +1,8 @@
1
+ from collections import defaultdict
1
2
  import os
2
3
  import warnings
3
4
  import importlib
5
+ import hashlib
4
6
  import multiprocessing
5
7
  from pathlib import Path
6
8
  from enum import Enum
@@ -8,7 +10,18 @@ from os import getenv
8
10
  from types import ModuleType
9
11
  from dataclasses import dataclass
10
12
  from functools import cached_property
11
- from typing import Any, Dict, List, Mapping, Optional, Self, Tuple, Union, Callable
13
+ from typing import (
14
+ Any,
15
+ Dict,
16
+ List,
17
+ Mapping,
18
+ Optional,
19
+ Self,
20
+ Tuple,
21
+ Union,
22
+ Callable,
23
+ cast,
24
+ )
12
25
 
13
26
  import yaml
14
27
  import jmespath
@@ -32,9 +45,6 @@ from sovereign.utils import dictupdate
32
45
  from sovereign.utils.version_info import compute_hash
33
46
 
34
47
  missing_arguments = {"missing", "positional", "arguments:"}
35
- BASIS = 2166136261
36
- PRIME = 16777619
37
- OVERFLOW = 0xFFFFFFFF
38
48
 
39
49
 
40
50
  class CacheStrategy(str, Enum):
@@ -89,44 +99,10 @@ class StatsdConfig(BaseModel):
89
99
  return ret
90
100
 
91
101
 
92
- class DiscoveryCacheConfig(BaseModel):
93
- enabled: bool = False
94
- host: str = "localhost"
95
- port: int = 6379
96
- secure: bool = False
97
- protocol: str = "redis://"
98
- password: SecretStr = SecretStr("")
99
- client_side: bool = True # True = Try in-memory cache before hitting redis
100
- wait_for_connection_timeout: int = 5
101
- socket_connect_timeout: int = 5
102
- socket_timeout: int = 5
103
- max_connections: int = 100
104
- retry_on_timeout: bool = True # Retry connections if they timeout.
105
- suppress: bool = False # False = Don't suppress connection errors. True = suppress connection errors
106
- socket_keepalive: bool = True # Try to keep connections to redis around.
107
- ttl: int = 60
108
- extra_keys: Dict[str, Any] = {}
109
-
110
- @model_validator(mode="after")
111
- def set_default_protocol(self) -> Self:
112
- if self.secure:
113
- self.protocol = "rediss://"
114
- return self
115
-
116
- @model_validator(mode="before")
117
- @classmethod
118
- def set_environmental_variables(cls, values: Dict[str, Any]) -> Dict[str, Any]:
119
- if host := getenv("SOVEREIGN_DISCOVERY_CACHE_REDIS_HOST"):
120
- values["host"] = host
121
- if port := getenv("SOVEREIGN_DISCOVERY_CACHE_REDIS_PORT"):
122
- values["port"] = int(port)
123
- if password := getenv("SOVEREIGN_DISCOVERY_CACHE_REDIS_PASSWORD"):
124
- values["password"] = SecretStr(password)
125
- return values
126
-
127
-
128
102
  class XdsTemplate(BaseModel):
129
103
  path: Union[str, Loadable]
104
+ resource_type: str
105
+ depends_on: list[str] = Field(default_factory=list)
130
106
 
131
107
  @property
132
108
  def loadable(self):
@@ -348,37 +324,14 @@ class DiscoveryRequest(BaseModel):
348
324
  def resources(self) -> Resources:
349
325
  return Resources(self.resource_names)
350
326
 
351
- @property
352
- def default_cache_rules(self):
353
- return [
354
- # Sovereign internal fields
355
- "template.version",
356
- "is_internal_request",
357
- "desired_controlplane",
358
- "resource_type",
359
- "api_version",
360
- "envoy_version",
361
- # Envoy fields from the real Discovery Request
362
- "resource_names",
363
- "node.cluster",
364
- "node.locality",
365
- ]
366
-
367
- def cache_key(self, rules: Optional[list[str]] = None):
368
- if rules is None:
369
- rules = self.default_cache_rules
370
- combined = 0
327
+ def cache_key(self, rules: list[str]) -> str:
371
328
  map = self.model_dump()
329
+ hash = hashlib.sha256()
372
330
  for expr in sorted(rules):
373
- value = jmespath.search(expr, map)
331
+ value = cast(str, jmespath.search(expr, map))
374
332
  val_str = f"{expr}={repr(value)}"
375
- # 32bit FNV hash
376
- h = BASIS
377
- for c in val_str:
378
- h = (h ^ ord(c)) * PRIME
379
- h &= OVERFLOW
380
- combined ^= h
381
- return combined
333
+ hash.update(val_str.encode())
334
+ return hash.hexdigest()
382
335
 
383
336
  @computed_field # type: ignore[prop-decorator]
384
337
  @cached_property
@@ -511,7 +464,6 @@ class SovereignConfig(BaseSettings):
511
464
  enable_access_logs: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
512
465
  log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
513
466
  ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
514
- discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
515
467
  tracing: Optional["TracingConfig"] = None
516
468
  model_config = SettingsConfigDict(
517
469
  env_file=".env",
@@ -541,7 +493,8 @@ class SovereignConfig(BaseSettings):
541
493
  } # Special key to hold templates from all versions
542
494
  for version, templates in self.templates.items():
543
495
  loaded_templates = {
544
- _type: XdsTemplate(path=path) for _type, path in templates.items()
496
+ _type: XdsTemplate(path=path, resource_type=_type)
497
+ for _type, path in templates.items()
545
498
  }
546
499
  ret[str(version)] = loaded_templates
547
500
  ret["__any__"].update(loaded_templates)
@@ -566,6 +519,7 @@ class SovereignConfig(BaseSettings):
566
519
  class TemplateSpecification(BaseModel):
567
520
  type: str
568
521
  spec: Loadable
522
+ depends_on: list[str] = Field(default_factory=list)
569
523
 
570
524
 
571
525
  class NodeMatching(BaseSettings):
@@ -671,6 +625,7 @@ class ContextConfiguration(BaseSettings):
671
625
  refresh_retry_interval_secs: int = Field(
672
626
  10, alias="SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
673
627
  )
628
+ cooldown: int = Field(15, alias="SOVEREIGN_CONTEXT_REFRESH_COOLDOWN")
674
629
  model_config = SettingsConfigDict(
675
630
  env_file=".env",
676
631
  extra="ignore",
@@ -717,7 +672,6 @@ class SourcesConfiguration(BaseSettings):
717
672
  refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
718
673
  max_retries: int = Field(3, alias="SOVEREIGN_SOURCES_MAX_RETRIES")
719
674
  retry_delay: int = Field(1, alias="SOVEREIGN_SOURCES_RETRY_DELAY")
720
- cache_strategy: Optional[Any] = None
721
675
  model_config = SettingsConfigDict(
722
676
  env_file=".env",
723
677
  extra="ignore",
@@ -852,29 +806,78 @@ class LegacyConfig(BaseSettings):
852
806
  return None
853
807
 
854
808
 
809
+ class TemplateConfiguration(BaseModel):
810
+ default: list[TemplateSpecification]
811
+ versions: dict[str, list[TemplateSpecification]] = Field(default_factory=dict)
812
+
813
+
814
+ def default_hash_rules():
815
+ return [
816
+ # Sovereign internal fields
817
+ "template.version",
818
+ "is_internal_request",
819
+ "desired_controlplane",
820
+ "resource_type",
821
+ "api_version",
822
+ "envoy_version",
823
+ # Envoy fields from the real Discovery Request
824
+ "resource_names",
825
+ "node.cluster",
826
+ "node.locality",
827
+ ]
828
+
829
+
830
+ class CacheBackendConfig(BaseModel):
831
+ type: str = Field(..., description="Cache backend type (e.g., 'redis', 's3')")
832
+ config: dict[str, Any] = Field(
833
+ default_factory=dict, description="Backend-specific configuration"
834
+ )
835
+
836
+
837
+ class CacheConfiguration(BaseModel):
838
+ hash_rules: list[str] = Field(
839
+ default_factory=default_hash_rules,
840
+ description="The set of JMES expressions against incoming Discovery Requests used to form a cache key.",
841
+ )
842
+ read_timeout: float = Field(
843
+ 5.0,
844
+ description="How long to block when trying to read from the cache before giving up",
845
+ )
846
+ local_fs_path: str = Field(
847
+ "/var/run/sovereign_cache",
848
+ description="Local filesystem cache path. Used to provide fast responses to clients and reduce hits against remote cache backend.",
849
+ )
850
+ remote_backend: CacheBackendConfig | None = Field(
851
+ None, description="Remote cache backend configuration"
852
+ )
853
+
854
+
855
855
  class SovereignConfigv2(BaseSettings):
856
- templates: Dict[str, List[TemplateSpecification]]
856
+ # Config generation
857
+ templates: TemplateConfiguration
857
858
  template_context: ContextConfiguration = ContextConfiguration()
859
+
860
+ # Web/Discovery
858
861
  authentication: AuthConfiguration = AuthConfiguration()
859
- logging: LoggingConfiguration = LoggingConfiguration()
860
- statsd: StatsdConfig = StatsdConfig()
861
- sentry_dsn: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_SENTRY_DSN")
862
- discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
863
862
 
864
- # Worker stuff
865
- caching_rules: Optional[list[str]] = None
866
- cache_path: str = Field("/var/run/sovereign_cache", alias="SOVEREIGN_CACHE_PATH")
867
- cache_timeout: float = Field(5.0, alias="SOVEREIGN_CACHE_READ_TIMEOUT")
863
+ # Cache
864
+ cache: CacheConfiguration = CacheConfiguration()
865
+
866
+ # Worker
868
867
  worker_host: Optional[str] = Field("localhost", alias="SOVEREIGN_WORKER_HOST")
869
868
  worker_port: Optional[int] = Field(9080, alias="SOVEREIGN_WORKER_PORT")
870
869
 
871
- tracing: Optional[TracingConfig] = Field(default_factory=TracingConfig)
872
- debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
873
-
874
870
  # Supervisord settings
875
871
  supervisord: SupervisordConfig = SupervisordConfig()
876
872
 
877
- # Deprecated in 0.30
873
+ # Misc
874
+ tracing: Optional[TracingConfig] = Field(default_factory=TracingConfig)
875
+ debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
876
+ logging: LoggingConfiguration = LoggingConfiguration()
877
+ statsd: StatsdConfig = StatsdConfig()
878
+ sentry_dsn: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_SENTRY_DSN")
879
+
880
+ # Planned for removal/deprecated/blocked by circular context usage internally
878
881
  sources: Optional[List[ConfiguredSource]] = Field(None, deprecated=True)
879
882
  source_config: SourcesConfiguration = Field(
880
883
  default_factory=SourcesConfiguration, deprecated=True
@@ -884,6 +887,8 @@ class SovereignConfigv2(BaseSettings):
884
887
  )
885
888
  modifiers: List[str] = Field(default_factory=list, deprecated=True)
886
889
  global_modifiers: List[str] = Field(default_factory=list, deprecated=True)
890
+
891
+ # Deprecated, need to migrate off internally
887
892
  legacy_fields: LegacyConfig = Field(default_factory=LegacyConfig, deprecated=True)
888
893
 
889
894
  model_config = SettingsConfigDict(
@@ -898,16 +903,23 @@ class SovereignConfigv2(BaseSettings):
898
903
  return self.authentication.auth_passwords.get_secret_value().split(",") or []
899
904
 
900
905
  def xds_templates(self) -> Dict[str, Dict[str, XdsTemplate]]:
901
- ret: Dict[str, Dict[str, XdsTemplate]] = {
902
- "__any__": {}
903
- } # Special key to hold templates from all versions
904
- for version, template_specs in self.templates.items():
905
- loaded_templates = {
906
- template.type: XdsTemplate(path=template.spec)
907
- for template in template_specs
908
- }
909
- ret[str(version)] = loaded_templates
910
- ret["__any__"].update(loaded_templates)
906
+ ret: Dict[str, Dict[str, XdsTemplate]] = defaultdict(dict)
907
+ for template in self.templates.default:
908
+ ret["default"][template.type] = XdsTemplate(
909
+ path=template.spec,
910
+ resource_type=template.type,
911
+ depends_on=template.depends_on,
912
+ )
913
+ for version, templates in self.templates.versions.items():
914
+ for template in templates:
915
+ loaded = XdsTemplate(
916
+ path=template.spec,
917
+ resource_type=template.type,
918
+ depends_on=template.depends_on,
919
+ )
920
+ ret[version][template.type] = loaded
921
+ ret["__any__"][template.type] = loaded
922
+ ret["__any__"].update(ret["default"])
911
923
  return ret
912
924
 
913
925
  def __str__(self) -> str:
@@ -921,7 +933,7 @@ class SovereignConfigv2(BaseSettings):
921
933
 
922
934
  @staticmethod
923
935
  def from_legacy_config(other: SovereignConfig) -> "SovereignConfigv2":
924
- new_templates = dict()
936
+ new_templates = TemplateConfiguration(default=list())
925
937
  for version, templates in other.templates.items():
926
938
  specs = list()
927
939
  for type, path in templates.items():
@@ -934,14 +946,16 @@ class SovereignConfigv2(BaseSettings):
934
946
  else:
935
947
  # Just in case? Although this shouldn't happen
936
948
  specs.append(TemplateSpecification(type=type, spec=path))
937
- new_templates[str(version)] = specs
949
+ if version == "default":
950
+ new_templates.default = specs
951
+ else:
952
+ new_templates.versions[str(version)] = specs
938
953
 
939
954
  return SovereignConfigv2(
940
955
  sources=other.sources,
941
956
  templates=new_templates,
942
957
  source_config=SourcesConfiguration(
943
958
  refresh_rate=other.sources_refresh_rate,
944
- cache_strategy=None,
945
959
  ),
946
960
  modifiers=other.modifiers,
947
961
  global_modifiers=other.global_modifiers,
@@ -983,7 +997,6 @@ class SovereignConfigv2(BaseSettings):
983
997
  dns_hard_fail=other.dns_hard_fail,
984
998
  environment=other.environment,
985
999
  ),
986
- discovery_cache=other.discovery_cache,
987
1000
  )
988
1001
 
989
1002
 
@@ -1030,20 +1043,9 @@ def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
1030
1043
 
1031
1044
 
1032
1045
  config_path = os.getenv("SOVEREIGN_CONFIG", "file:///etc/sovereign.yaml")
1033
- try:
1034
- config = SovereignConfigv2(**parse_raw_configuration(config_path))
1035
- except ValidationError:
1036
- old_config = SovereignConfig(**parse_raw_configuration(config_path))
1037
- config = SovereignConfigv2.from_legacy_config(old_config)
1046
+ config = SovereignConfigv2(**parse_raw_configuration(config_path))
1038
1047
 
1039
1048
  XDS_TEMPLATES = config.xds_templates()
1040
- try:
1041
- default_templates = XDS_TEMPLATES["default"]
1042
- except KeyError:
1043
- warnings.warn(
1044
- "Your configuration should contain default templates. For more details, see "
1045
- "https://developer.atlassian.com/platform/sovereign/tutorial/templates/#versioning-templates"
1046
- )
1047
1049
 
1048
1050
  # Create an enum that bases all the available discovery types off what has been configured
1049
1051
  discovery_types = (_type for _type in sorted(XDS_TEMPLATES["__any__"].keys()))
sovereign/server.py CHANGED
@@ -6,16 +6,15 @@ from pathlib import Path
6
6
  import uvicorn
7
7
 
8
8
  from sovereign import application_logger as log
9
- from sovereign.app import app
10
- from sovereign.worker import worker as worker_app
11
9
  from sovereign.schemas import SovereignAsgiConfig, SupervisordConfig
12
10
 
13
-
14
11
  asgi_config = SovereignAsgiConfig()
15
12
  supervisord_config = SupervisordConfig()
16
13
 
17
14
 
18
15
  def web() -> None:
16
+ from sovereign.app import app
17
+
19
18
  log.debug("Starting web server")
20
19
  uvicorn.run(
21
20
  app,
@@ -30,6 +29,8 @@ def web() -> None:
30
29
 
31
30
 
32
31
  def worker():
32
+ from sovereign.worker import worker as worker_app
33
+
33
34
  log.debug("Starting worker")
34
35
  uvicorn.run(
35
36
  worker_app,
@@ -13,7 +13,7 @@ from sovereign.schemas import ConfiguredSource, SourceData, Node, config
13
13
  from sovereign.utils.entry_point_loader import EntryPointLoader
14
14
  from sovereign.sources.lib import Source
15
15
  from sovereign.modifiers.lib import Modifier, GlobalModifier
16
- from sovereign.context import NEW_CONTEXT
16
+ from sovereign.events import bus, Topic, Event
17
17
 
18
18
  from structlog.stdlib import BoundLogger
19
19
 
@@ -206,6 +206,7 @@ class SourcePoller:
206
206
  self.source_data_modified: SourceData = SourceData()
207
207
  self.last_updated = datetime.now()
208
208
  self.instance_count = 0
209
+ self.initialized = False
209
210
 
210
211
  self.cache: dict[str, dict[str, list[dict[str, Any]]]] = {}
211
212
  self.registry: set[Any] = set()
@@ -483,17 +484,32 @@ class SourcePoller:
483
484
  self.cache[node_value] = result
484
485
  return result
485
486
 
486
- def poll(self) -> None:
487
+ async def poll(self) -> None:
487
488
  updated = self.refresh()
488
489
  self.source_data_modified = self.apply_modifications(self.source_data)
490
+ if not self.initialized:
491
+ await bus.publish(
492
+ Topic.CONTEXT,
493
+ Event(
494
+ message="Sources initialized",
495
+ metadata={"name": "sources"},
496
+ ),
497
+ )
498
+ self.initialized = True
489
499
  if updated:
490
500
  self.cache.clear()
491
- NEW_CONTEXT.set()
501
+ await bus.publish(
502
+ Topic.CONTEXT,
503
+ Event(
504
+ message="Sources refreshed",
505
+ metadata={"name": "sources"},
506
+ ),
507
+ )
492
508
 
493
509
  async def poll_forever(self) -> None:
494
510
  while True:
495
511
  try:
496
- self.poll()
512
+ await self.poll()
497
513
 
498
514
  # If we have retry count, use exponential backoff for next attempt
499
515
  if self.retry_count > 0:
sovereign/statistics.py CHANGED
@@ -51,7 +51,7 @@ def configure_statsd() -> StatsDProxy:
51
51
  module = DogStatsd()
52
52
  if config.enabled and module:
53
53
  module.host = config.host
54
- module.port = config.port
54
+ module.port = int(config.port)
55
55
  module.namespace = config.namespace
56
56
  module.use_ms = config.use_ms
57
57
  for tag, value in config.tags.items():