-
Notifications
You must be signed in to change notification settings - Fork 9
On-disk cache #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
On-disk cache #67
Changes from all commits
b5d7b1a
74f0aa7
0ee9d9f
1ccc34c
a98ef4c
2b3bec4
2944e48
98ea830
9722bc4
375d78f
a760351
b90d491
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import functools | ||
| import os | ||
| import pickle | ||
| import sqlite3 | ||
| import asyncstdlib as a | ||
|
|
||
| USE_CACHE = True if os.getenv("NO_CACHE") != "1" else False | ||
| CACHE_LOCATION = ( | ||
| os.path.expanduser( | ||
| os.getenv("CACHE_LOCATION", "~/.cache/async-substrate-interface") | ||
| ) | ||
| if USE_CACHE | ||
| else ":memory:" | ||
| ) | ||
|
|
||
|
|
||
| def _get_table_name(func): | ||
| """Convert "ClassName.method_name" to "ClassName_method_name""" | ||
| return func.__qualname__.replace(".", "_") | ||
|
|
||
|
|
||
| def _check_if_local(chain: str) -> bool: | ||
| return any([x in chain for x in ["127.0.0.1", "localhost", "0.0.0.0"]]) | ||
|
|
||
|
|
||
| def _create_table(c, conn, table_name): | ||
| c.execute( | ||
| f"""CREATE TABLE IF NOT EXISTS {table_name} | ||
| ( | ||
| rowid INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| key BLOB, | ||
| value BLOB, | ||
| chain TEXT, | ||
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||
| ); | ||
| """ | ||
| ) | ||
| c.execute( | ||
| f"""CREATE TRIGGER IF NOT EXISTS prune_rows_trigger AFTER INSERT ON {table_name} | ||
| BEGIN | ||
| DELETE FROM {table_name} | ||
| WHERE rowid IN ( | ||
| SELECT rowid FROM {table_name} | ||
| ORDER BY created_at DESC | ||
| LIMIT -1 OFFSET 500 | ||
| ); | ||
| END;""" | ||
| ) | ||
| conn.commit() | ||
|
|
||
|
|
||
| def _retrieve_from_cache(c, table_name, key, chain): | ||
| try: | ||
| c.execute( | ||
| f"SELECT value FROM {table_name} WHERE key=? AND chain=?", (key, chain) | ||
| ) | ||
| result = c.fetchone() | ||
| if result is not None: | ||
| return pickle.loads(result[0]) | ||
| except (pickle.PickleError, sqlite3.Error) as e: | ||
| print(f"Cache error: {str(e)}") | ||
| pass | ||
|
|
||
|
|
||
| def _insert_into_cache(c, conn, table_name, key, result, chain): | ||
| try: | ||
| c.execute( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. connection context manager? |
||
| f"INSERT OR REPLACE INTO {table_name} (key, value, chain) VALUES (?,?,?)", | ||
| (key, pickle.dumps(result), chain), | ||
| ) | ||
| conn.commit() | ||
| except (pickle.PickleError, sqlite3.Error) as e: | ||
| print(f"Cache error: {str(e)}") | ||
| pass | ||
|
|
||
|
|
||
| def sql_lru_cache(maxsize=None): | ||
| def decorator(func): | ||
| conn = sqlite3.connect(CACHE_LOCATION) | ||
| c = conn.cursor() | ||
| table_name = _get_table_name(func) | ||
| _create_table(c, conn, table_name) | ||
|
|
||
| @functools.lru_cache(maxsize=maxsize) | ||
| def inner(self, *args, **kwargs): | ||
| c = conn.cursor() | ||
| key = pickle.dumps((args, kwargs)) | ||
| chain = self.url | ||
| if not (local_chain := _check_if_local(chain)) or not USE_CACHE: | ||
| result = _retrieve_from_cache(c, table_name, key, chain) | ||
| if result is not None: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it possible to cache There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. But none of the cached methods can ever return |
||
| return result | ||
|
|
||
| # If not in DB, call func and store in DB | ||
| result = func(self, *args, **kwargs) | ||
|
|
||
| if not local_chain or not USE_CACHE: | ||
| _insert_into_cache(c, conn, table_name, key, result, chain) | ||
|
|
||
| return result | ||
|
|
||
| return inner | ||
|
|
||
| return decorator | ||
|
|
||
|
|
||
| def async_sql_lru_cache(maxsize=None): | ||
| def decorator(func): | ||
| conn = sqlite3.connect(CACHE_LOCATION) | ||
| c = conn.cursor() | ||
| table_name = _get_table_name(func) | ||
| _create_table(c, conn, table_name) | ||
|
|
||
| @a.lru_cache(maxsize=maxsize) | ||
| async def inner(self, *args, **kwargs): | ||
| c = conn.cursor() | ||
| key = pickle.dumps((args, kwargs)) | ||
| chain = self.url | ||
|
|
||
| if not (local_chain := _check_if_local(chain)) or not USE_CACHE: | ||
| result = _retrieve_from_cache(c, table_name, key, chain) | ||
| if result is not None: | ||
| return result | ||
|
|
||
| # If not in DB, call func and store in DB | ||
| result = await func(self, *args, **kwargs) | ||
| if not local_chain or not USE_CACHE: | ||
| _insert_into_cache(c, conn, table_name, key, result, chain) | ||
|
|
||
| return result | ||
|
|
||
| return inner | ||
|
|
||
| return decorator | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can simply use Connection ctx manager:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this isn't available on 3.9, but can double-check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it is available, but do we want to do that rather than just reuse the same cursor object repeatedly?