Skip to content

Commit d52f20f

Browse files
authored
Merge pull request #259 from opentensor/release/1.6.0
Release/1.6.0
2 parents a3e7369 + aa8b583 commit d52f20f

8 files changed

Lines changed: 362 additions & 65 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 1.6.0 /2025-01-27
4+
* Fix typo by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/258
5+
* Improve Disk Caching by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/227
6+
7+
**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.5.15...v1.6.0
8+
39
## 1.5.15 /2025-12-22
410
* Modifies the CachedFetcher to not keep pending exceptions by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/253
511

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pip install async-substrate-interface
1313

1414
## Usage
1515

16-
Here are examples of how to use the sync and async inferfaces:
16+
Here are examples of how to use the sync and async interfaces:
1717

1818
```python
1919
from async_substrate_interface import SubstrateInterface

async_substrate_interface/async_substrate.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,10 @@ async def __aenter__(self):
11811181
await self.initialize()
11821182
return self
11831183

1184-
async def initialize(self):
1184+
async def initialize(self) -> None:
1185+
await self._initialize()
1186+
1187+
async def _initialize(self) -> None:
11851188
"""
11861189
Initialize the connection to the chain.
11871190
"""
@@ -1206,7 +1209,7 @@ async def initialize(self):
12061209
self._initializing = False
12071210

12081211
async def __aexit__(self, exc_type, exc_val, exc_tb):
1209-
await self.ws.shutdown()
1212+
await self.close()
12101213

12111214
@property
12121215
def metadata(self):
@@ -2428,7 +2431,6 @@ async def get_block_metadata(
24282431
"MetadataVersioned", data=ScaleBytes(result)
24292432
)
24302433
metadata_decoder.decode()
2431-
24322434
return metadata_decoder
24332435
else:
24342436
return result
@@ -4289,20 +4291,27 @@ async def _handler(block_data: dict[str, Any]):
42894291

42904292
class DiskCachedAsyncSubstrateInterface(AsyncSubstrateInterface):
42914293
"""
4292-
Experimental new class that uses disk-caching in addition to memory-caching for the cached methods
4294+
Uses disk-caching in addition to memory-caching for the cached methods
4295+
4296+
Loads the cache from the disk at startup, where it is kept in-memory, and dumps to the disk
4297+
when the connection is closed.
42934298
"""
42944299

4300+
async def initialize(self) -> None:
4301+
await self.runtime_cache.load_from_disk(self.url)
4302+
await self._initialize()
4303+
42954304
async def close(self):
42964305
"""
4297-
Closes the substrate connection, and the websocket connection.
4306+
Closes the substrate connection and the websocket connection, dumps the runtime cache to disk
42984307
"""
42994308
try:
4309+
await self.runtime_cache.dump_to_disk(self.url)
43004310
await self.ws.shutdown()
43014311
except AttributeError:
43024312
pass
4303-
db_conn = AsyncSqliteDB(self.url)
4304-
if db_conn._db is not None:
4305-
await db_conn._db.close()
4313+
db = AsyncSqliteDB(self.url)
4314+
await db.close()
43064315

43074316
@async_sql_lru_cache(maxsize=SUBSTRATE_CACHE_METHOD_SIZE)
43084317
async def get_parent_block_hash(self, block_hash):

async_substrate_interface/types.py

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from abc import ABC
33
from collections import defaultdict, deque
44
from collections.abc import Iterable
5+
from contextlib import suppress
56
from dataclasses import dataclass
67
from datetime import datetime
78
from typing import Optional, Union, Any
@@ -16,6 +17,7 @@
1617

1718
from .const import SS58_FORMAT
1819
from .utils import json
20+
from .utils.cache import AsyncSqliteDB
1921

2022
logger = logging.getLogger("async_substrate_interface")
2123

@@ -34,8 +36,8 @@ class RuntimeCache:
3436
is important you are utilizing the correct version.
3537
"""
3638

37-
blocks: dict[int, "Runtime"]
38-
block_hashes: dict[str, "Runtime"]
39+
blocks: dict[int, str]
40+
block_hashes: dict[str, int]
3941
versions: dict[int, "Runtime"]
4042
last_used: Optional["Runtime"]
4143

@@ -56,10 +58,10 @@ def add_item(
5658
Adds a Runtime object to the cache mapped to its version, block number, and/or block hash.
5759
"""
5860
self.last_used = runtime
59-
if block is not None:
60-
self.blocks[block] = runtime
61-
if block_hash is not None:
62-
self.block_hashes[block_hash] = runtime
61+
if block is not None and block_hash is not None:
62+
self.blocks[block] = block_hash
63+
if block_hash is not None and runtime_version is not None:
64+
self.block_hashes[block_hash] = runtime_version
6365
if runtime_version is not None:
6466
self.versions[runtime_version] = runtime
6567

@@ -73,33 +75,52 @@ def retrieve(
7375
Retrieves a Runtime object from the cache, using the key of its block number, block hash, or runtime version.
7476
Retrieval happens in this order. If no Runtime is found mapped to any of your supplied keys, returns `None`.
7577
"""
78+
runtime = None
7679
if block is not None:
77-
runtime = self.blocks.get(block)
78-
if runtime is not None:
79-
if block_hash is not None:
80-
# if lookup occurs for block_hash and block, but only block matches, also map to block_hash
81-
self.add_item(runtime, block_hash=block_hash)
80+
if block_hash is not None:
81+
self.blocks[block] = block_hash
82+
if runtime_version is not None:
83+
self.block_hashes[block_hash] = runtime_version
84+
with suppress(KeyError):
85+
runtime = self.versions[self.block_hashes[self.blocks[block]]]
8286
self.last_used = runtime
8387
return runtime
8488
if block_hash is not None:
85-
runtime = self.block_hashes.get(block_hash)
86-
if runtime is not None:
87-
if block is not None:
88-
# if lookup occurs for block_hash and block, but only block_hash matches, also map to block
89-
self.add_item(runtime, block=block)
89+
if runtime_version is not None:
90+
self.block_hashes[block_hash] = runtime_version
91+
with suppress(KeyError):
92+
runtime = self.versions[self.block_hashes[block_hash]]
9093
self.last_used = runtime
9194
return runtime
9295
if runtime_version is not None:
93-
runtime = self.versions.get(runtime_version)
94-
if runtime is not None:
95-
# if runtime_version matches, also map to block and block_hash (if supplied)
96-
if block is not None:
97-
self.add_item(runtime, block=block)
98-
if block_hash is not None:
99-
self.add_item(runtime, block_hash=block_hash)
96+
with suppress(KeyError):
97+
runtime = self.versions[runtime_version]
10098
self.last_used = runtime
10199
return runtime
102-
return None
100+
return runtime
101+
102+
async def load_from_disk(self, chain_endpoint: str):
103+
db = AsyncSqliteDB(chain_endpoint=chain_endpoint)
104+
(
105+
block_mapping,
106+
block_hash_mapping,
107+
runtime_version_mapping,
108+
) = await db.load_runtime_cache(chain_endpoint)
109+
if not any([block_mapping, block_hash_mapping, runtime_version_mapping]):
110+
logger.debug("No runtime mappings in disk cache")
111+
else:
112+
logger.debug("Found runtime mappings in disk cache")
113+
self.blocks = block_mapping
114+
self.block_hashes = block_hash_mapping
115+
self.versions = {
116+
x: Runtime.deserialize(y) for x, y in runtime_version_mapping.items()
117+
}
118+
119+
async def dump_to_disk(self, chain_endpoint: str):
120+
db = AsyncSqliteDB(chain_endpoint=chain_endpoint)
121+
await db.dump_runtime_cache(
122+
chain_endpoint, self.blocks, self.block_hashes, self.versions
123+
)
103124

104125

105126
class Runtime:
@@ -149,6 +170,45 @@ def __init__(
149170
if registry is not None:
150171
self.load_registry_type_map()
151172

173+
def serialize(self):
174+
metadata_value = self.metadata.data.data
175+
return {
176+
"chain": self.chain,
177+
"type_registry": self.type_registry,
178+
"metadata_value": metadata_value,
179+
"metadata_v15": self.metadata_v15.encode_to_metadata_option(),
180+
"runtime_info": {
181+
"specVersion": self.runtime_version,
182+
"transactionVersion": self.transaction_version,
183+
},
184+
"registry": self.registry.registry if self.registry is not None else None,
185+
"ss58_format": self.ss58_format,
186+
}
187+
188+
@classmethod
189+
def deserialize(cls, serialized: dict) -> "Runtime":
190+
ss58_format = serialized["ss58_format"]
191+
runtime_config = RuntimeConfigurationObject(ss58_format=ss58_format)
192+
runtime_config.clear_type_registry()
193+
runtime_config.update_type_registry(load_type_registry_preset(name="core"))
194+
metadata = runtime_config.create_scale_object(
195+
"MetadataVersioned", data=ScaleBytes(serialized["metadata_value"])
196+
)
197+
metadata.decode()
198+
registry = PortableRegistry.from_json(serialized["registry"])
199+
return cls(
200+
chain=serialized["chain"],
201+
metadata=metadata,
202+
type_registry=serialized["type_registry"],
203+
runtime_config=runtime_config,
204+
metadata_v15=MetadataV15.decode_from_metadata_option(
205+
serialized["metadata_v15"]
206+
),
207+
registry=registry,
208+
ss58_format=ss58_format,
209+
runtime_info=serialized["runtime_info"],
210+
)
211+
152212
def load_runtime(self):
153213
"""
154214
Initial loading of the runtime's type registry information.

0 commit comments

Comments
 (0)