Skip to content

Commit e53ae1d

Browse files
committed
Address review: topology JSON on domain classes, cache read tests, fix ts_names_for
- Move topology serialization onto the domain classes: add Topology.to_json/from_json and ServerNode.to_json/from_json; the topology cache now uses them instead of local helpers. - Add end-to-end read-path coverage for the topology cache (previously only creation/write was tested): a serialization round-trip test and a seed-from-cache test that boots a store against an unreachable url and asserts the topology is loaded from disk. The seed path now also restores the cached topology etag. - test_time_series_names_for: the server is fine - an empty timeSeriesNamesFor array indexes as a blank term on Corax but not on Lucene. Unskip and force the static index engine to Lucene (matching the other Corax-limited tests) instead of skipping.
1 parent 7b66853 commit e53ae1d

6 files changed

Lines changed: 85 additions & 37 deletions

File tree

ravendb/http/request_executor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ def __run(errors: list):
470470
cached_topology = self._try_load_topology_from_cache(url)
471471
if cached_topology is not None:
472472
self._node_selector = NodeSelector(cached_topology, self._thread_pool_executor)
473+
self._topology_etag = cached_topology.etag
473474
self.__initialize_update_topology_timer()
474475
self.__topology_taken_from_node = ServerNode(url, self._database_name)
475476
return

ravendb/http/server_node.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ def __hash__(self) -> int:
4747
def last_server_version(self) -> str:
4848
return self.__last_server_version
4949

50+
def to_json(self) -> dict:
51+
return {
52+
"Url": self.url,
53+
"Database": self.database,
54+
"ClusterTag": self.cluster_tag,
55+
"ServerRole": self.server_role.value if self.server_role is not None else None,
56+
}
57+
58+
@classmethod
59+
def from_json(cls, json_dict: dict) -> "ServerNode":
60+
role = json_dict.get("ServerRole")
61+
return cls(
62+
json_dict.get("Url"),
63+
json_dict.get("Database"),
64+
json_dict.get("ClusterTag"),
65+
cls.Role(role) if role else None,
66+
)
67+
5068
@classmethod
5169
def create_from(cls, topology: "ClusterTopology"):
5270
nodes = []

ravendb/http/topology.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ def __init__(self, etag: int, nodes: List[ServerNode]):
2525
self.etag = etag
2626
self.nodes = nodes
2727

28+
def to_json(self) -> Dict:
29+
return {"Etag": self.etag, "Nodes": [node.to_json() for node in (self.nodes or [])]}
30+
31+
@classmethod
32+
def from_json(cls, json_dict: Dict) -> "Topology":
33+
return cls(
34+
json_dict.get("Etag"),
35+
[ServerNode.from_json(node_json) for node_json in (json_dict.get("Nodes") or [])],
36+
)
37+
2838

2939
class ClusterTopology:
3040
def __init__(self):

ravendb/http/topology_local_cache.py

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import os
66
from typing import Optional
77

8-
from ravendb.http.server_node import ServerNode
98
from ravendb.http.topology import Topology
109

1110
# On-disk topology cache (mirrors the .NET client's TopologyLocalCache). The feature is
@@ -24,44 +23,14 @@ def _path(location: str, topology_hash: str, extension: str) -> str:
2423
return os.path.join(location, topology_hash + extension)
2524

2625

27-
def _topology_to_json(topology: Topology) -> dict:
28-
return {
29-
"Etag": topology.etag,
30-
"Nodes": [
31-
{
32-
"Url": node.url,
33-
"Database": node.database,
34-
"ClusterTag": node.cluster_tag,
35-
"ServerRole": node.server_role.value if node.server_role is not None else None,
36-
}
37-
for node in (topology.nodes or [])
38-
],
39-
}
40-
41-
42-
def _topology_from_json(data: dict) -> Topology:
43-
nodes = []
44-
for node_json in data.get("Nodes") or []:
45-
role = node_json.get("ServerRole")
46-
nodes.append(
47-
ServerNode(
48-
node_json.get("Url"),
49-
node_json.get("Database"),
50-
node_json.get("ClusterTag"),
51-
ServerNode.Role(role) if role else None,
52-
)
53-
)
54-
return Topology(data.get("Etag"), nodes)
55-
56-
5726
def try_save(location: Optional[str], topology_hash: str, topology: Topology, extension: str) -> None:
5827
# Best-effort: caching failures must never break a topology update.
5928
if not location or topology is None:
6029
return
6130
try:
6231
os.makedirs(location, exist_ok=True)
6332
with open(_path(location, topology_hash, extension), "w", encoding="utf-8") as stream:
64-
json.dump(_topology_to_json(topology), stream)
33+
json.dump(topology.to_json(), stream)
6534
except Exception:
6635
pass
6736

@@ -74,6 +43,6 @@ def try_load(location: Optional[str], topology_hash: str, extension: str) -> Opt
7443
if not os.path.isfile(path):
7544
return None
7645
with open(path, "r", encoding="utf-8") as stream:
77-
return _topology_from_json(json.load(stream))
46+
return Topology.from_json(json.load(stream))
7847
except Exception:
7948
return None

ravendb/tests/jvm_migrated_tests/client_tests/indexing_tests/time_series_tests/test_basic_time_series_indexes_java_script.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ class TestBasicTimeSeriesIndexesJavaScript(TestBase):
2424
def setUp(self):
2525
super(TestBasicTimeSeriesIndexesJavaScript, self).setUp()
2626

27-
@unittest.skip(
28-
"JS time-series index: timeSeriesNamesFor emits a 'names' term before any time series "
29-
"exists on 7.2.x (Corax term generation differs from the original Lucene expectation)"
30-
)
27+
def _customize_db_record(self, db_record):
28+
# An empty timeSeriesNamesFor array indexes as a (blank) term on Corax but produces no term on
29+
# Lucene; force the static index engine to Lucene so the "no time series -> no names" expectation holds.
30+
db_record.settings["Indexing.Static.SearchEngineType"] = "Lucene"
31+
3132
def test_time_series_names_for(self):
3233
now = RavenTestHelper.utc_today()
3334
index = Companies_ByTimeSeriesNames()

ravendb/tests/system_tests/test_system_create_topology_files.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from shutil import rmtree
55

66
from ravendb.documents.store.definition import DocumentStore
7+
from ravendb.http import topology_local_cache
8+
from ravendb.http.server_node import ServerNode
9+
from ravendb.http.topology import Topology
710
from ravendb.serverwide.database_record import DatabaseRecord
811
from ravendb.serverwide.operations.common import DeleteDatabaseOperation, CreateDatabaseOperation
912
from ravendb.tests.test_base import TestBase
@@ -60,6 +63,52 @@ def test_topology_creation(self):
6063
os.path.exists(os.path.join(TOPOLOGY_FILES_DIR, cluster_topology_hash + ".raven-cluster-topology"))
6164
)
6265

66+
def test_topology_cache_round_trip(self):
67+
os.makedirs(TOPOLOGY_FILES_DIR, exist_ok=True)
68+
topology = Topology(3, [ServerNode("http://localhost:9999", "db1", "B", ServerNode.Role.MEMBER)])
69+
topology_hash = topology_local_cache.server_hash("http://localhost:9999", "db1")
70+
topology_local_cache.try_save(
71+
TOPOLOGY_FILES_DIR, topology_hash, topology, topology_local_cache.DATABASE_TOPOLOGY_EXTENSION
72+
)
73+
74+
loaded = topology_local_cache.try_load(
75+
TOPOLOGY_FILES_DIR, topology_hash, topology_local_cache.DATABASE_TOPOLOGY_EXTENSION
76+
)
77+
self.assertIsNotNone(loaded)
78+
self.assertEqual(3, loaded.etag)
79+
self.assertEqual(1, len(loaded.nodes))
80+
node = loaded.nodes[0]
81+
self.assertEqual("http://localhost:9999", node.url)
82+
self.assertEqual("db1", node.database)
83+
self.assertEqual("B", node.cluster_tag)
84+
self.assertEqual(ServerNode.Role.MEMBER, node.server_role)
85+
86+
def test_topology_is_loaded_from_cache_when_urls_unreachable(self):
87+
bad_url = "http://127.0.0.1:1"
88+
database = "CacheSeedTest"
89+
os.makedirs(TOPOLOGY_FILES_DIR, exist_ok=True)
90+
cached = Topology(9, [ServerNode(bad_url, database, "A", ServerNode.Role.MEMBER)])
91+
topology_local_cache.try_save(
92+
TOPOLOGY_FILES_DIR,
93+
topology_local_cache.server_hash(bad_url, database),
94+
cached,
95+
topology_local_cache.DATABASE_TOPOLOGY_EXTENSION,
96+
)
97+
98+
# the server is unreachable, so the first topology update must fall back to the on-disk cache
99+
with DocumentStore(urls=[bad_url], database=database) as store:
100+
store.conventions.topology_cache_location = TOPOLOGY_FILES_DIR
101+
store.initialize()
102+
request_executor = store.get_request_executor()
103+
request_executor._first_topology_update_task.result(30)
104+
105+
nodes = request_executor.topology_nodes
106+
self.assertEqual(1, len(nodes))
107+
self.assertEqual("A", nodes[0].cluster_tag)
108+
self.assertEqual(bad_url, nodes[0].url)
109+
self.assertEqual(ServerNode.Role.MEMBER, nodes[0].server_role)
110+
self.assertEqual(9, request_executor.topology_etag)
111+
63112

64113
if __name__ == "__main__":
65114
unittest.main()

0 commit comments

Comments
 (0)