-
Notifications
You must be signed in to change notification settings - Fork 20
421 optimise indexer transaction receipt storage #423
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
Changes from 3 commits
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,27 @@ | ||
| import psycopg2 | ||
| import os | ||
|
|
||
| POSTGRES_DB = os.environ.get("POSTGRES_DB", "neon-db") | ||
| POSTGRES_USER = os.environ.get("POSTGRES_USER", "neon-proxy") | ||
| POSTGRES_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "neon-proxy-pass") | ||
| POSTGRES_HOST = os.environ.get("POSTGRES_HOST", "localhost") | ||
|
|
||
| try: | ||
| from cPickle import dumps, loads, HIGHEST_PROTOCOL as PICKLE_PROTOCOL | ||
| except ImportError: | ||
| from pickle import dumps, loads, HIGHEST_PROTOCOL as PICKLE_PROTOCOL | ||
|
|
||
|
|
||
| def encode(obj): | ||
| """Serialize an object using pickle to a binary format accepted by SQLite.""" | ||
| return psycopg2.Binary(dumps(obj, protocol=PICKLE_PROTOCOL)) | ||
|
|
||
|
|
||
| def decode(obj): | ||
| """Deserialize objects retrieved from SQLite.""" | ||
| return loads(bytes(obj)) | ||
|
|
||
|
|
||
| def dummy(obj): | ||
| """Does nothing""" | ||
| return obj |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import psycopg2 | ||
| import os | ||
| import logging | ||
| from proxy.indexer.pg_common import POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD\ | ||
| , POSTGRES_HOST, encode, decode, dummy | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| class TrxReceiptsStorage: | ||
| def __init__(self, table_name, log_level = logging.DEBUG): | ||
| self.table_name = table_name | ||
| logger.setLevel(log_level) | ||
| self.conn = psycopg2.connect( | ||
| dbname=POSTGRES_DB, | ||
| user=POSTGRES_USER, | ||
| password=POSTGRES_PASSWORD, | ||
| host=POSTGRES_HOST | ||
| ) | ||
|
|
||
| self.conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) | ||
| cur = self.conn.cursor() | ||
| cur.execute(f''' | ||
| CREATE TABLE IF NOT EXISTS | ||
| {self.table_name} ( | ||
| slot BIGINT, | ||
| signature VARCHAR(88), | ||
| trx BYTEA, | ||
| PRIMARY KEY (slot, signature) | ||
| ) | ||
| ''') | ||
|
|
||
| def clear(self): | ||
| cur = self.conn.cursor() | ||
| cur.execute(f'DELETE FROM {self.table_name}') | ||
|
|
||
| def size(self): | ||
| cur = self.conn.cursor() | ||
| cur.execute(f'SELECT COUNT(*) FROM {self.table_name}') | ||
| rows = cur.fetchone()[0] | ||
| return rows if rows is not None else 0 | ||
|
|
||
| def max_known_trx(self): | ||
| cur = self.conn.cursor() | ||
| cur.execute(f'SELECT slot, signature FROM {self.table_name} ORDER BY slot DESC, signature DESC LIMIT 1') | ||
| row = cur.fetchone() | ||
| if row is not None: | ||
| return (row[0], row[1]) | ||
| return (0, None) #table empty - return default value | ||
|
|
||
| def add_trx(self, slot, signature, trx): | ||
| bin_trx = encode(trx) | ||
| cur = self.conn.cursor() | ||
| cur.execute(f''' | ||
| INSERT INTO {self.table_name} (slot, signature, trx) | ||
| VALUES ({slot},%s,%s) | ||
| ON CONFLICT (slot, signature) | ||
| DO UPDATE SET | ||
| trx = EXCLUDED.trx | ||
| ''', | ||
| (signature, bin_trx) | ||
| ) | ||
|
|
||
| def contains(self, slot, signature): | ||
| cur = self.conn.cursor() | ||
| cur.execute(f'SELECT 1 FROM {self.table_name} WHERE slot = %s AND signature = %s', (slot, signature,)) | ||
| return cur.fetchone() is not None | ||
|
|
||
| def get_trxs(self, start_slot = 0, reverse = False): | ||
|
Contributor
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. Annotations?
Contributor
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. You don't need extra spaces around
|
||
| cur = self.conn.cursor() | ||
| order = 'DESC' if reverse else 'ASC' | ||
| cur.execute(f'SELECT slot, signature, trx FROM {self.table_name} WHERE slot >= {start_slot} ORDER BY slot {order}') | ||
| rows = cur.fetchall() | ||
| for row in rows: | ||
| yield row[0], row[1], decode(row[2]) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| from unittest import TestCase | ||
| from proxy.indexer.trx_receipts_storage import TrxReceiptsStorage | ||
| from random import randint | ||
| from base58 import b58encode | ||
|
|
||
|
|
||
| class TestTrxReceiptsStorage(TestCase): | ||
| @classmethod | ||
| def setUpClass(cls) -> None: | ||
| print("\n\nhttps://github.com/neonlabsorg/proxy-model.py/issues/421") | ||
| cls.testee = TrxReceiptsStorage('test_storage') | ||
|
|
||
| def create_signature(self): | ||
| signature = b'' | ||
| for i in range(0, 5): | ||
| signature += randint(0, 255).to_bytes(1, byteorder='big') | ||
| return b58encode(signature).decode("utf-8") | ||
|
|
||
| def create_slot_sig(self, max_slot): | ||
| slot = randint(0, max_slot) | ||
|
Contributor
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. random slot makes test fail "randomly"
Contributor
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.
Contributor
Author
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. Thank you! This is due to probabilistic reasons: sometimes generated data not contains any transaction for given slot. In average, it will exist only two transaction for every given slot with current generation parameters (100 trxs in 50 slots randomly), but in reality there a cases when there are no transactions for given slot at all. Actually, testing expression must cover such cases, so I changed it. |
||
| return (slot, self.create_signature()) | ||
|
|
||
| def test_data_consistency(self): | ||
| """ | ||
| Test that data put into container is stored there | ||
| """ | ||
| self.testee.clear() | ||
| self.assertEqual(self.testee.size(), 0) | ||
| self.assertEqual(self.testee.max_known_trx(), (0, None)) | ||
|
|
||
| max_slot = 10 | ||
| num_items = 100 | ||
| expected_items = [] | ||
| for _ in range(0, num_items): | ||
| slot, signature = self.create_slot_sig(max_slot) | ||
| trx = { 'slot': slot, 'signature': signature } | ||
| self.testee.add_trx(slot, signature, trx) | ||
| expected_items.append((slot, signature, trx)) | ||
|
|
||
| self.assertEqual(self.testee.max_known_trx()[0], max_slot) | ||
| self.assertEqual(self.testee.size(), num_items) | ||
| for item in expected_items: | ||
| self.assertTrue(self.testee.contains(item[0], item[1])) | ||
|
|
||
| def test_query(self): | ||
| """ | ||
| Test get_trxs method workds as expected | ||
| """ | ||
| self.testee.clear() | ||
| self.assertEqual(self.testee.size(), 0) | ||
|
|
||
| max_slot = 50 | ||
| num_items = 100 | ||
| expected_items = [] | ||
| for _ in range(0, num_items): | ||
| slot, signature = self.create_slot_sig(max_slot) | ||
| trx = { 'slot': slot, 'signature': signature } | ||
| self.testee.add_trx(slot, signature, trx) | ||
| expected_items.append((slot, signature, trx)) | ||
|
|
||
| start_slot = 34 | ||
| retrieved_trxs = [item for item in self.testee.get_trxs(start_slot, False)] | ||
| self.assertEqual(retrieved_trxs[0][0], start_slot) | ||
| self.assertEqual(retrieved_trxs[-1][0], max_slot) | ||
|
|
||
| retrieved_trxs = [item for item in self.testee.get_trxs(start_slot, True)] | ||
| self.assertEqual(retrieved_trxs[0][0], max_slot) | ||
| self.assertEqual(retrieved_trxs[-1][0], start_slot) | ||
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.