Skip to content

Commit e6fbf60

Browse files
Heartbeat REST endpoint (#211)
* Fix processing modified storage models * --force flag for hasura configure * Fix schema init, apply builtin scripts * comment * Add built-in rest endpoint * Renamings, filter head status by name * Changelog * Fix view condition
1 parent 58b1947 commit e6fbf60

File tree

6 files changed

+56
-14
lines changed

6 files changed

+56
-14
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ Please use [this](https://docs.gitlab.com/ee/development/changelog.html) documen
77
### Added
88

99
* cli: Added `schema init` command to initialize database schema.
10+
* cli: Added `--force` flag to `hasura configure` command.
1011
* codegen: Added support for subpackages inside callback directories.
12+
* hasura: Added `dipdup_head_status` view and REST endpoint.
1113
* index: Added an ability to skip historical data while synchronizing `big_map` indexes.
1214
* metadata: Added `metadata` datasource.
1315
* tzkt: Added `get_big_map` and `get_contract_big_maps` datasource methods.

src/dipdup/cli.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from dipdup.models import Index
5050
from dipdup.models import Schema
5151
from dipdup.utils import iter_files
52+
from dipdup.utils.database import execute_sql_scripts
5253
from dipdup.utils.database import set_decimal_context
5354
from dipdup.utils.database import tortoise_wrapper
5455
from dipdup.utils.database import wipe_schema
@@ -304,9 +305,10 @@ async def hasura(ctx):
304305

305306

306307
@hasura.command(name='configure', help='Configure Hasura GraphQL Engine')
308+
@click.option('--force', is_flag=True, help='Proceed even if Hasura is already configured')
307309
@click.pass_context
308310
@cli_wrapper
309-
async def hasura_configure(ctx):
311+
async def hasura_configure(ctx, force: bool):
310312
config: DipDupConfig = ctx.obj.config
311313
url = config.database.connection_string
312314
models = f'{config.package}.models'
@@ -320,7 +322,7 @@ async def hasura_configure(ctx):
320322

321323
async with tortoise_wrapper(url, models):
322324
async with hasura_gateway:
323-
await hasura_gateway.configure()
325+
await hasura_gateway.configure(force)
324326

325327

326328
@cli.group(help='Manage database schema')
@@ -403,8 +405,14 @@ async def schema_init(ctx):
403405
async with AsyncExitStack() as stack:
404406
await dipdup._set_up_database(stack)
405407
await dipdup._set_up_hooks()
408+
await dipdup._create_datasources()
406409
await dipdup._initialize_schema()
407410

411+
# NOTE: It's not necessary a reindex, but it's safe to execute built-in scripts to (re)create views.
412+
conn = get_connection(None)
413+
sql_path = join(dirname(__file__), 'sql', 'on_reindex')
414+
await execute_sql_scripts(conn, sql_path)
415+
408416
_logger.info('Schema initialized')
409417

410418

src/dipdup/context.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from typing import Union
1717
from typing import cast
1818

19-
import sqlparse # type: ignore
2019
from tortoise import Tortoise
2120
from tortoise.exceptions import OperationalError
2221
from tortoise.transactions import get_connection
@@ -50,7 +49,7 @@
5049
from dipdup.models import ReindexingReason
5150
from dipdup.models import Schema
5251
from dipdup.utils import FormattedLogger
53-
from dipdup.utils import iter_files
52+
from dipdup.utils.database import execute_sql_scripts
5453
from dipdup.utils.database import wipe_schema
5554

5655
pending_indexes = deque() # type: ignore
@@ -333,14 +332,7 @@ async def execute_sql(self, ctx: 'DipDupContext', name: str) -> None:
333332

334333
# NOTE: SQL hooks are executed on default connection
335334
connection = get_connection(None)
336-
337-
for file in iter_files(sql_path, '.sql'):
338-
ctx.logger.info('Executing `%s`', file.name)
339-
sql = file.read()
340-
for statement in sqlparse.split(sql):
341-
# NOTE: Ignore empty statements
342-
with suppress(AttributeError):
343-
await connection.execute_script(statement)
335+
await execute_sql_scripts(connection, sql_path)
344336

345337
@contextmanager
346338
def _wrapper(self, kind: str, name: str) -> Iterator[None]:

src/dipdup/hasura.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def __init__(
102102
self._hasura_config = hasura_config
103103
self._database_config = database_config
104104

105-
async def configure(self) -> None:
105+
async def configure(self, force: bool = False) -> None:
106106
"""Generate Hasura metadata and apply to instance with credentials from `hasura` config section."""
107107

108108
if self._database_config.schema_name != 'public':
@@ -116,7 +116,7 @@ async def configure(self) -> None:
116116
metadata = await self._fetch_metadata()
117117
metadata_hash = self._hash_metadata(metadata)
118118

119-
if hasura_schema.hash == metadata_hash:
119+
if not force and hasura_schema.hash == metadata_hash:
120120
self._logger.info('Metadata is up to date, no action required')
121121
return
122122

@@ -342,6 +342,9 @@ async def _generate_query_collections_metadata(self) -> List[Dict[str, Any]]:
342342
for query_name, query in self._iterate_graphql_queries():
343343
queries.append({'name': query_name, 'query': query})
344344

345+
# NOTE: This is the only view we add by ourselves and thus know all params. Won't work for any view.
346+
queries.append(self._format_rest_head_status_query())
347+
345348
return queries
346349

347350
async def _generate_rest_endpoints_metadata(self, query_names: List[str]) -> List[Dict[str, Any]]:
@@ -455,6 +458,16 @@ def _format_rest_query(self, name: str, table: str, filter: str, fields: Iterabl
455458
'query': 'query ' + name + ' (' + query_arg + ') {' + table + '(' + query_filter + ') {' + query_fields + '}}',
456459
}
457460

461+
def _format_rest_head_status_query(self) -> Dict[str, Any]:
462+
name = 'dipdup_head_status'
463+
if self._hasura_config.camel_case:
464+
name = humps.camelize(name)
465+
466+
return {
467+
'name': name,
468+
'query': 'query ' + name + ' ($name: String!) {' + name + '(where: {name: {_eq: $name}}) {status}}',
469+
}
470+
458471
def _format_rest_endpoint(self, query_name: str) -> Dict[str, Any]:
459472
return {
460473
"definition": {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE
2+
OR REPLACE VIEW dipdup_head_status AS
3+
SELECT
4+
name,
5+
CASE
6+
WHEN timestamp < NOW() - interval '2 minutes' THEN 'OUTDATED'
7+
ELSE 'OK'
8+
END AS status
9+
FROM
10+
dipdup_head;

src/dipdup/utils/database.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import importlib
55
import logging
66
from contextlib import asynccontextmanager
7+
from contextlib import suppress
78
from enum import Enum
89
from os.path import dirname
910
from os.path import join
@@ -16,6 +17,7 @@
1617
from typing import Type
1718
from typing import Union
1819

20+
import sqlparse # type: ignore
1921
from tortoise import Model
2022
from tortoise import Tortoise
2123
from tortoise.backends.asyncpg.client import AsyncpgDBClient
@@ -30,6 +32,7 @@
3032

3133
from dipdup.enums import ReversedEnum
3234
from dipdup.exceptions import DatabaseConfigurationError
35+
from dipdup.utils import iter_files
3336
from dipdup.utils import pascal_to_snake
3437

3538
_logger = logging.getLogger('dipdup.database')
@@ -144,13 +147,27 @@ async def create_schema(conn: BaseDBAsyncClient, name: str) -> None:
144147
await conn.execute_script(_truncate_schema_sql)
145148

146149

150+
async def execute_sql_scripts(conn: BaseDBAsyncClient, path: str) -> None:
151+
for file in iter_files(path, '.sql'):
152+
_logger.info('Executing `%s`', file.name)
153+
sql = file.read()
154+
for statement in sqlparse.split(sql):
155+
# NOTE: Ignore empty statements
156+
with suppress(AttributeError):
157+
await conn.execute_script(statement)
158+
159+
147160
async def generate_schema(conn: BaseDBAsyncClient, name: str) -> None:
148161
if isinstance(conn, SqliteClient):
149162
await Tortoise.generate_schemas()
150163
elif isinstance(conn, AsyncpgDBClient):
151164
await create_schema(conn, name)
152165
await set_schema(conn, name)
153166
await Tortoise.generate_schemas()
167+
168+
# NOTE: Apply built-in scripts before project ones
169+
sql_path = join(dirname(__file__), '..', 'sql', 'on_reindex')
170+
await execute_sql_scripts(conn, sql_path)
154171
else:
155172
raise NotImplementedError
156173

0 commit comments

Comments
 (0)