|
17 | 17 | from typing import DefaultDict |
18 | 18 | from typing import Deque |
19 | 19 | from typing import Dict |
| 20 | +from typing import Generator |
20 | 21 | from typing import List |
21 | 22 | from typing import NoReturn |
22 | 23 | from typing import Optional |
@@ -293,6 +294,21 @@ def __init__( |
293 | 294 | def request_limit(self) -> int: |
294 | 295 | return cast(int, self._http_config.batch_size) |
295 | 296 |
|
| 297 | + def get_channel_level(self, message_type: MessageType) -> int: |
| 298 | + """Get current level of the channel, or sync level is no messages were received yet.""" |
| 299 | + channel_level = self._level[message_type] |
| 300 | + if channel_level is None: |
| 301 | + # NOTE: If no data messages were received since run, use sync level instead |
| 302 | + # NOTE: There's only one sync level for all channels, otherwise `Index.process` would fail |
| 303 | + channel_level = self.get_sync_level(HeadSubscription()) |
| 304 | + if channel_level is None: |
| 305 | + raise RuntimeError('Neither current nor sync level is known') |
| 306 | + |
| 307 | + return channel_level |
| 308 | + |
| 309 | + def _set_channel_level(self, message_type: MessageType, level: int) -> None: |
| 310 | + self._level[message_type] = level |
| 311 | + |
296 | 312 | async def get_similar_contracts( |
297 | 313 | self, |
298 | 314 | address: str, |
@@ -795,66 +811,78 @@ async def _extract_message_data(self, type_: MessageType, message: List[Any]) -> |
795 | 811 | if tzkt_type == TzktMessageType.STATE: |
796 | 812 | continue |
797 | 813 |
|
798 | | - message_level, current_level = item['state'], self._level[type_] |
799 | | - self._level[type_] = message_level |
| 814 | + message_level = item['state'] |
| 815 | + channel_level = self.get_channel_level(type_) |
| 816 | + self._set_channel_level(type_, message_level) |
| 817 | + |
800 | 818 | self._logger.info( |
801 | 819 | 'Realtime message received: %s, %s, %s -> %s', |
802 | 820 | type_.value, |
803 | 821 | tzkt_type.name, |
804 | | - current_level, |
| 822 | + channel_level, |
805 | 823 | message_level, |
806 | 824 | ) |
807 | 825 |
|
808 | 826 | # NOTE: Put data messages to buffer by level |
809 | 827 | if tzkt_type == TzktMessageType.DATA: |
810 | | - self._buffer[message_level].append((type_, item['data'])) |
| 828 | + await self._process_data_message(type_, message_level, item['data']) |
811 | 829 |
|
812 | 830 | # NOTE: Try to process rollback automatically, emit if failed |
813 | 831 | elif tzkt_type == TzktMessageType.REORG: |
814 | | - # NOTE: operation/big_map channels have their own levels |
815 | | - if type_ == MessageType.head: |
816 | | - return |
817 | | - |
818 | | - # NOTE: If no data messages were received since run, use sync level instead |
819 | | - if current_level is None: |
820 | | - # NOTE: There's only one sync level for all channels, otherwise `Index.process` would fail |
821 | | - current_level = self.get_sync_level(HeadSubscription()) |
822 | | - if not current_level: |
823 | | - raise RuntimeError('Reorg message received, but neither current nor sync level is known') |
824 | | - |
825 | | - # NOTE: This rollback does not affect us, so we can safely ignore it |
826 | | - if current_level <= message_level: |
827 | | - return |
828 | | - |
829 | | - self._logger.info('Rollback requested from %s to %s', current_level, message_level) |
830 | | - |
831 | | - # NOTE: Drop buffered messages in reversed order while possible |
832 | | - rolled_back_levels = range(current_level, message_level, -1) |
833 | | - for rolled_back_level in rolled_back_levels: |
834 | | - if self._buffer.pop(rolled_back_level, None): |
835 | | - self._logger.info('Level %s is buffered', rolled_back_level) |
836 | | - else: |
837 | | - self._logger.info('Level %s is not buffered, emitting rollback', rolled_back_level) |
838 | | - await self.emit_rollback(current_level, message_level) |
839 | | - return |
840 | | - |
841 | | - self._logger.info('Rollback is not required, continuing') |
| 832 | + await self._process_reorg_message(type_, channel_level, message_level) |
842 | 833 |
|
843 | 834 | else: |
844 | | - raise NotImplementedError |
| 835 | + raise NotImplementedError('Unknown message type') |
845 | 836 |
|
846 | 837 | # NOTE: Yield extensive data from buffer |
| 838 | + for item in self._yield_from_buffer(type_): |
| 839 | + yield item |
| 840 | + |
| 841 | + def _yield_from_buffer(self, type_: MessageType) -> Generator[Dict, None, None]: |
847 | 842 | buffered_levels = sorted(self._buffer.keys()) |
848 | | - emitted_levels = buffered_levels[: len(buffered_levels) - self._buffer_size] |
| 843 | + if len(buffered_levels) < self._buffer_size: |
| 844 | + return |
849 | 845 |
|
850 | | - for level in emitted_levels: |
| 846 | + yielded_levels = buffered_levels[: len(buffered_levels) - self._buffer_size] |
| 847 | + for level in yielded_levels: |
851 | 848 | for idx, level_data in enumerate(self._buffer[level]): |
852 | 849 | level_message_type, level_message = level_data |
853 | 850 | if level_message_type == type_: |
854 | 851 | yield level_message |
855 | 852 | self._buffer[level].pop(idx) |
| 853 | + |
856 | 854 | if not self._buffer[level]: |
857 | | - self._buffer.pop(level) |
| 855 | + del self._buffer[level] |
| 856 | + |
| 857 | + async def _process_data_message(self, type_: MessageType, message_level: int, message_data: Dict[str, Any]) -> None: |
| 858 | + self._buffer[message_level].append((type_, message_data)) |
| 859 | + |
| 860 | + async def _process_reorg_message(self, type_: MessageType, channel_level: int, message_level: int) -> None: |
| 861 | + # NOTE: No action required for this channel |
| 862 | + if type_ == MessageType.head: |
| 863 | + return |
| 864 | + |
| 865 | + # NOTE: This rollback does not affect us, so we can safely ignore it |
| 866 | + if channel_level <= message_level: |
| 867 | + return |
| 868 | + |
| 869 | + self._logger.info('Rollback requested from %s to %s', channel_level, message_level) |
| 870 | + |
| 871 | + # NOTE: Drop buffered messages in reversed order while possible |
| 872 | + rolled_back_levels = range(channel_level, message_level, -1) |
| 873 | + for rolled_back_level in rolled_back_levels: |
| 874 | + if self._buffer.pop(rolled_back_level, None): |
| 875 | + self._logger.info('Level %s is buffered', rolled_back_level) |
| 876 | + else: |
| 877 | + self._logger.info( |
| 878 | + 'Level %s is not buffered, emitting rollback to %s', |
| 879 | + rolled_back_level, |
| 880 | + message_level, |
| 881 | + ) |
| 882 | + await self.emit_rollback(channel_level, message_level) |
| 883 | + return |
| 884 | + else: |
| 885 | + self._logger.info('Rollback is not required, continuing') |
858 | 886 |
|
859 | 887 | async def _on_operations_message(self, message: List[Dict[str, Any]]) -> None: |
860 | 888 | """Parse and emit raw operations from WS""" |
|
0 commit comments