Skip to content

Commit 3f6437b

Browse files
Fix HTTPGateway, reset Hasura on configuration failure (#118)
* Perf counter * CLI help * Fix ratelimiting in HTTPGateway * Hasura reset option * Fix bugs in retrier * Cleanup * Cleanup
1 parent 64fb454 commit 3f6437b

File tree

10 files changed

+148
-107
lines changed

10 files changed

+148
-107
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,6 @@ cython_debug/
145145

146146
.noseids
147147

148-
secrets.env
148+
secrets.env
149+
150+
*.out*

src/dipdup/cli.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,22 @@ def _bump_spec_version(spec_version: str):
160160
raise ConfigurationError('Unknown `spec_version`')
161161

162162

163+
# TODO: "cache clear"?
163164
@cli.command(help='Clear development request cache')
164165
@click.pass_context
165166
@cli_wrapper
166167
async def clear_cache(ctx):
167168
FileCache('dipdup', flag='cs').clear()
168169

169170

170-
@cli.group()
171+
@cli.group(help='Docker integration related commands')
171172
@click.pass_context
172173
@cli_wrapper
173174
async def docker(ctx):
174175
...
175176

176177

177-
@docker.command(name='init')
178+
@docker.command(name='init', help='Generate Docker inventory in project directory')
178179
@click.option('--image', '-i', type=str, help='DipDup Docker image', default=DEFAULT_DOCKER_IMAGE)
179180
@click.option('--tag', '-t', type=str, help='DipDup Docker tag', default=DEFAULT_DOCKER_TAG)
180181
@click.option('--env-file', '-e', type=str, help='Path to env_file', default=DEFAULT_DOCKER_ENV_FILE)
@@ -185,7 +186,7 @@ async def docker_init(ctx, image: str, tag: str, env_file: str):
185186
await DipDupCodeGenerator(config, {}).generate_docker(image, tag, env_file)
186187

187188

188-
@cli.group()
189+
@cli.group(help='Hasura integration related commands')
189190
@click.pass_context
190191
@cli_wrapper
191192
async def hasura(ctx):
@@ -201,10 +202,15 @@ async def hasura_configure(ctx, reset: bool):
201202
url = config.database.connection_string
202203
models = f'{config.package}.models'
203204
if not config.hasura:
204-
_logger.error('`hasura` config section is empty')
205-
return
206-
hasura_gateway = HasuraGateway(config.package, config.hasura, cast(PostgresDatabaseConfig, config.database))
205+
raise ConfigurationError('`hasura` config section is empty')
206+
if reset:
207+
config.hasura.reset = True
208+
hasura_gateway = HasuraGateway(
209+
package=config.package,
210+
hasura_config=config.hasura,
211+
database_config=cast(PostgresDatabaseConfig, config.database),
212+
)
207213

208214
async with tortoise_wrapper(url, models):
209215
async with hasura_gateway:
210-
await hasura_gateway.configure(reset)
216+
await hasura_gateway.configure()

src/dipdup/config.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
from abc import ABC, abstractmethod
99
from collections import defaultdict
10+
from copy import copy
1011
from enum import Enum
1112
from os import environ as env
1213
from os.path import dirname
@@ -101,12 +102,13 @@ class HTTPConfig:
101102
connection_limit: Optional[int] = None
102103
batch_size: Optional[int] = None
103104

104-
def merge(self, other: Optional['HTTPConfig']) -> None:
105-
if not other:
106-
return
107-
for k, v in other.__dict__.items():
108-
if v is not None:
109-
setattr(self, k, v)
105+
def merge(self, other: Optional['HTTPConfig']) -> 'HTTPConfig':
106+
config = copy(self)
107+
if other:
108+
for k, v in other.__dict__.items():
109+
if v is not None:
110+
setattr(config, k, v)
111+
return config
110112

111113

112114
@dataclass
@@ -659,6 +661,7 @@ class HasuraConfig:
659661
camel_case: bool = False
660662
connection_timeout: int = 5
661663
rest: bool = True
664+
reset: Optional[bool] = None
662665
http: Optional[HTTPConfig] = None
663666

664667
@validator('url', allow_reuse=True)

src/dipdup/datasources/bcd/datasource.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,22 @@
88

99

1010
class BcdDatasource(Datasource):
11+
_default_http_config = HTTPConfig(
12+
cache=True,
13+
retry_sleep=1,
14+
retry_multiplier=1.1,
15+
ratelimit_rate=100,
16+
ratelimit_period=30,
17+
connection_limit=25,
18+
)
19+
1120
def __init__(
1221
self,
1322
url: str,
1423
network: str,
1524
http_config: Optional[HTTPConfig] = None,
1625
) -> None:
17-
super().__init__(url, http_config)
26+
super().__init__(url, self._default_http_config.merge(http_config))
1827
self._logger = logging.getLogger('dipdup.bcd')
1928
self._network = network
2029

@@ -45,13 +54,3 @@ async def get_token(self, address: str, token_id: int) -> Optional[Dict[str, Any
4554
if response:
4655
return response[0]
4756
return None
48-
49-
def _default_http_config(self) -> HTTPConfig:
50-
return HTTPConfig(
51-
cache=True,
52-
retry_sleep=1,
53-
retry_multiplier=1.1,
54-
ratelimit_rate=100,
55-
ratelimit_period=30,
56-
connection_limit=25,
57-
)

src/dipdup/datasources/coinbase/datasource.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,16 @@
1111

1212

1313
class CoinbaseDatasource(Datasource):
14+
_default_http_config = HTTPConfig(
15+
cache=True,
16+
retry_count=3,
17+
retry_sleep=1,
18+
ratelimit_rate=10,
19+
ratelimit_period=1,
20+
)
21+
1422
def __init__(self, url: str = API_URL, http_config: Optional[HTTPConfig] = None) -> None:
15-
super().__init__(url, http_config)
23+
super().__init__(url, self._default_http_config.merge(http_config))
1624
self._logger = logging.getLogger('dipdup.coinbase')
1725

1826
async def run(self) -> None:
@@ -43,15 +51,6 @@ async def get_candles(self, since: datetime, until: datetime, interval: CandleIn
4351
candles += [CandleData.from_json(c) for c in candles_json]
4452
return sorted(candles, key=lambda c: c.timestamp)
4553

46-
def _default_http_config(self) -> HTTPConfig:
47-
return HTTPConfig(
48-
cache=True,
49-
retry_count=3,
50-
retry_sleep=1,
51-
ratelimit_rate=10,
52-
ratelimit_period=1,
53-
)
54-
5554
def _split_candle_requests(self, since: datetime, until: datetime, interval: CandleInterval) -> List[Tuple[datetime, datetime]]:
5655
request_interval_limit = timedelta(seconds=interval.seconds * CANDLES_REQUEST_LIMIT)
5756
request_intervals = []

src/dipdup/datasources/datasource.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import abstractmethod
22
from enum import Enum
33
from logging import Logger
4-
from typing import Awaitable, List, Optional, Protocol
4+
from typing import Awaitable, List, Protocol
55

66
from pyee import AsyncIOEventEmitter # type: ignore
77

@@ -46,7 +46,7 @@ async def run(self) -> None:
4646

4747

4848
class IndexDatasource(Datasource, AsyncIOEventEmitter):
49-
def __init__(self, url: str, http_config: Optional[HTTPConfig] = None) -> None:
49+
def __init__(self, url: str, http_config: HTTPConfig) -> None:
5050
HTTPGateway.__init__(self, url, http_config)
5151
AsyncIOEventEmitter.__init__(self)
5252

src/dipdup/datasources/tzkt/datasource.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -288,12 +288,21 @@ class TzktDatasource(IndexDatasource):
288288
* Calls Matchers to match received operation groups with indexes' pattern and spawn callbacks on match
289289
"""
290290

291+
_default_http_config = HTTPConfig(
292+
cache=True,
293+
retry_sleep=1,
294+
retry_multiplier=1.1,
295+
ratelimit_rate=100,
296+
ratelimit_period=30,
297+
connection_limit=25,
298+
)
299+
291300
def __init__(
292301
self,
293302
url: str,
294303
http_config: Optional[HTTPConfig] = None,
295304
) -> None:
296-
super().__init__(url, http_config)
305+
super().__init__(url, self._default_http_config.merge(http_config))
297306
self._logger = logging.getLogger('dipdup.tzkt')
298307

299308
self._transaction_subscriptions: Set[str] = set()
@@ -623,21 +632,11 @@ async def _subscribe_to_head(self) -> None:
623632
[],
624633
)
625634

626-
def _default_http_config(self) -> HTTPConfig:
627-
return HTTPConfig(
628-
cache=True,
629-
retry_sleep=1,
630-
retry_multiplier=1.1,
631-
ratelimit_rate=100,
632-
ratelimit_period=30,
633-
connection_limit=25,
634-
)
635-
636635
async def _extract_message_data(self, channel: str, message: List[Any]) -> Any:
637636
for item in message:
638637
head_level = item['state']
639638
message_type = TzktMessageType(item['type'])
640-
self._logger.debug('`%s` message: %s', channel, message_type.name)
639+
self._logger.debug('`%s` message: %s', channel, message_type.name)
641640

642641
if message_type == TzktMessageType.STATE:
643642
if self._sync_level != head_level:
@@ -657,7 +656,6 @@ async def _extract_message_data(self, channel: str, message: List[Any]) -> Any:
657656
else:
658657
raise NotImplementedError
659658

660-
661659
async def _on_operation_message(self, message: List[Dict[str, Any]]) -> None:
662660
"""Parse and emit raw operations from WS"""
663661
async for data in self._extract_message_data('operation', message):

src/dipdup/hasura.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import logging
44
import re
55
from contextlib import suppress
6+
from http import HTTPStatus
67
from os.path import dirname, join
78
from typing import Any, Dict, Iterator, List, Optional, Tuple
89

10+
import aiohttp
911
import humps # type: ignore
1012
from aiohttp import ClientConnectorError, ClientOSError
1113
from pydantic.dataclasses import dataclass
@@ -60,26 +62,32 @@ class HasuraError(RuntimeError):
6062

6163

6264
class HasuraGateway(HTTPGateway):
65+
_default_http_config = HTTPConfig(
66+
cache=False,
67+
retry_count=3,
68+
retry_sleep=1,
69+
)
70+
6371
def __init__(
6472
self,
6573
package: str,
6674
hasura_config: HasuraConfig,
6775
database_config: PostgresDatabaseConfig,
6876
http_config: Optional[HTTPConfig] = None,
6977
) -> None:
70-
super().__init__(hasura_config.url, http_config)
78+
super().__init__(hasura_config.url, self._default_http_config.merge(http_config))
7179
self._logger = logging.getLogger('dipdup.hasura')
7280
self._package = package
7381
self._hasura_config = hasura_config
7482
self._database_config = database_config
7583

76-
async def configure(self, reset: bool = False) -> None:
84+
async def configure(self) -> None:
7785
"""Generate Hasura metadata and apply to instance with credentials from `hasura` config section."""
7886

7987
self._logger.info('Configuring Hasura')
8088
await self._healthcheck()
8189

82-
if reset:
90+
if self._hasura_config.reset is True:
8391
await self._reset_metadata()
8492

8593
metadata = await self._fetch_metadata()
@@ -132,15 +140,6 @@ async def configure(self, reset: bool = False) -> None:
132140

133141
self._logger.info('Hasura instance has been configured')
134142

135-
def _default_http_config(self) -> HTTPConfig:
136-
return HTTPConfig(
137-
cache=False,
138-
retry_sleep=1,
139-
retry_multiplier=1.1,
140-
ratelimit_rate=100,
141-
ratelimit_period=1,
142-
)
143-
144143
async def _hasura_request(self, endpoint: str, json: Dict[str, Any]) -> Dict[str, Any]:
145144
self._logger.debug('Sending `%s` request: %s', endpoint, json)
146145
result = await self._http.request(
@@ -188,13 +187,22 @@ async def _fetch_metadata(self) -> Dict[str, Any]:
188187

189188
async def _replace_metadata(self, metadata: Dict[str, Any]) -> None:
190189
self._logger.info('Replacing metadata')
191-
await self._hasura_request(
192-
endpoint='query',
193-
json={
194-
"type": "replace_metadata",
195-
"args": metadata,
196-
},
197-
)
190+
endpoint, json = 'query', {
191+
"type": "replace_metadata",
192+
"args": metadata,
193+
}
194+
try:
195+
await self._hasura_request(endpoint, json)
196+
except aiohttp.ClientResponseError as e:
197+
# NOTE: 400 from Hasura means we failed either to generate or to merge existing metadata.
198+
# NOTE: Reset metadata and retry if not forbidden by config.
199+
print(e.status, self._hasura_config.reset)
200+
if e.status != HTTPStatus.BAD_REQUEST or self._hasura_config.reset is False:
201+
print('raise')
202+
raise
203+
self._logger.warning('Failed to replace metadata, resetting')
204+
await self._reset_metadata()
205+
await self._hasura_request(endpoint, json)
198206

199207
async def _get_views(self) -> List[str]:
200208
return [

0 commit comments

Comments
 (0)