abstract-block-dumper 0.0.9__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/CHANGELOG.md +10 -0
  2. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/PKG-INFO +1 -1
  3. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/pyproject.toml +3 -0
  4. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/dal/django_dal.py +15 -13
  5. abstract_block_dumper-0.1.1/src/abstract_block_dumper/_internal/providers/bittensor_client.py +78 -0
  6. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/services/backfill_scheduler.py +2 -2
  7. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/services/block_processor.py +24 -3
  8. abstract_block_dumper-0.1.1/src/abstract_block_dumper/_internal/services/scheduler.py +136 -0
  9. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/services/utils.py +1 -1
  10. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_version.py +2 -2
  11. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/v1/decorators.py +3 -1
  12. abstract_block_dumper-0.1.1/tests/conftest.py +115 -0
  13. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/integration/test_scheduler.py +34 -0
  14. abstract_block_dumper-0.1.1/tests/unit/test_decorator.py +0 -0
  15. abstract_block_dumper-0.0.9/src/abstract_block_dumper/_internal/services/scheduler.py +0 -195
  16. abstract_block_dumper-0.0.9/tests/conftest.py +0 -65
  17. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/.cruft.json +0 -0
  18. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/.github/dependabot.yml +0 -0
  19. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/.github/workflows/ci.yml +0 -0
  20. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/.github/workflows/publish.yml +0 -0
  21. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/.gitignore +0 -0
  22. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/.pre-commit-config.yaml +0 -0
  23. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/.shellcheckrc +0 -0
  24. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/README.md +0 -0
  25. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/SECURITY.md +0 -0
  26. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/docs/3rd_party/cookiecutter-rt-pkg/CHANGELOG.md +0 -0
  27. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/.dockerignore +0 -0
  28. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/.gitignore +0 -0
  29. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/Dockerfile +0 -0
  30. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/README.md +0 -0
  31. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/__init__.py +0 -0
  32. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/admin.py +0 -0
  33. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/apps.py +0 -0
  34. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/management/__init__.py +0 -0
  35. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/management/commands/__init__.py +0 -0
  36. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/management/commands/create_admin.py +0 -0
  37. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/migrations/__init__.py +0 -0
  38. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/models.py +0 -0
  39. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/tasks.py +0 -0
  40. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/tests.py +0 -0
  41. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/block_explorer/views.py +0 -0
  42. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/docker-compose.yml +0 -0
  43. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/example_project/__init__.py +0 -0
  44. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/example_project/asgi.py +0 -0
  45. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/example_project/celery.py +0 -0
  46. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/example_project/settings.py +0 -0
  47. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/example_project/urls.py +0 -0
  48. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/example_project/wsgi.py +0 -0
  49. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/main.py +0 -0
  50. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/manage.py +0 -0
  51. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/pyproject.toml +0 -0
  52. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/pytest.ini +0 -0
  53. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/example_project/uv.lock +0 -0
  54. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/noxfile.py +0 -0
  55. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/__init__.py +0 -0
  56. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/__init__.py +0 -0
  57. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/dal/__init__.py +0 -0
  58. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/dal/memory_registry.py +0 -0
  59. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/discovery.py +0 -0
  60. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/exceptions.py +0 -0
  61. {abstract_block_dumper-0.0.9/src/abstract_block_dumper/_internal/services → abstract_block_dumper-0.1.1/src/abstract_block_dumper/_internal/providers}/__init__.py +0 -0
  62. {abstract_block_dumper-0.0.9/src/abstract_block_dumper/management → abstract_block_dumper-0.1.1/src/abstract_block_dumper/_internal/services}/__init__.py +0 -0
  63. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/services/executor.py +0 -0
  64. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/_internal/services/metrics.py +0 -0
  65. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/admin.py +0 -0
  66. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/apps.py +0 -0
  67. {abstract_block_dumper-0.0.9/src/abstract_block_dumper/management/commands → abstract_block_dumper-0.1.1/src/abstract_block_dumper/management}/__init__.py +0 -0
  68. {abstract_block_dumper-0.0.9/src/abstract_block_dumper/migrations → abstract_block_dumper-0.1.1/src/abstract_block_dumper/management/commands}/__init__.py +0 -0
  69. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/management/commands/backfill_blocks_v1.py +0 -0
  70. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/management/commands/block_tasks_v1.py +0 -0
  71. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/migrations/0001_initial.py +0 -0
  72. {abstract_block_dumper-0.0.9/src/abstract_block_dumper/v1 → abstract_block_dumper-0.1.1/src/abstract_block_dumper/migrations}/__init__.py +0 -0
  73. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/models.py +0 -0
  74. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/py.typed +0 -0
  75. {abstract_block_dumper-0.0.9/tests → abstract_block_dumper-0.1.1/src/abstract_block_dumper/v1}/__init__.py +0 -0
  76. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/v1/celery.py +0 -0
  77. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/src/abstract_block_dumper/v1/tasks.py +0 -0
  78. {abstract_block_dumper-0.0.9/tests/integration → abstract_block_dumper-0.1.1/tests}/__init__.py +0 -0
  79. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/django_fixtures.py +0 -0
  80. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/fatories.py +0 -0
  81. /abstract_block_dumper-0.0.9/tests/unit/test_decorator.py → /abstract_block_dumper-0.1.1/tests/integration/__init__.py +0 -0
  82. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/integration/test_backfill_scheduler.py +0 -0
  83. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/integration/test_block_processor.py +0 -0
  84. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/integration/test_concurrent_processing.py +0 -0
  85. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/integration/test_multi_arguments_tasks.py +0 -0
  86. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/integration/test_registered_celery_tasks.py +0 -0
  87. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/integration/test_task_registration.py +0 -0
  88. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/settings.py +0 -0
  89. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/tests/unit/test_celery_integration.py +0 -0
  90. {abstract_block_dumper-0.0.9 → abstract_block_dumper-0.1.1}/uv.lock +0 -0
@@ -9,6 +9,16 @@ upcoming release can be found in [changelog.d](changelog.d).
9
9
 
10
10
  <!-- towncrier release notes start -->
11
11
 
12
+ ## [0.1.1](https://github.com/bactensor/abstract-block-dumper/releases/tag/v0.1.1) - 2025-12-18
13
+
14
+ No significant changes.
15
+
16
+
17
+ ## [0.1.0](https://github.com/bactensor/abstract-block-dumper/releases/tag/v0.1.0) - 2025-12-10
18
+
19
+ No significant changes.
20
+
21
+
12
22
  ## [0.0.9](https://github.com/bactensor/abstract-block-dumper/releases/tag/v0.0.9) - 2025-12-04
13
23
 
14
24
  No significant changes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstract-block-dumper
3
- Version: 0.0.9
3
+ Version: 0.1.1
4
4
  Project-URL: Source, https://github.com/bactensor/abstract-block-dumper
5
5
  Project-URL: Issue Tracker, https://github.com/bactensor/abstract-block-dumper/issues
6
6
  Author-email: Reef Technologies <opensource@reef.pl>
@@ -144,6 +144,9 @@ directory = "infrastructure"
144
144
  name = "Infrastructure"
145
145
  showcontent = true
146
146
 
147
+ [tool.pyright]
148
+ typeCheckingMode = "off"
149
+
147
150
  [tool.mypy]
148
151
  plugins = ["mypy_django_plugin.main"]
149
152
  strict_optional = true
@@ -1,8 +1,10 @@
1
+ from collections.abc import Iterator
1
2
  from datetime import timedelta
2
3
  from typing import Any
3
4
 
4
5
  from django.conf import settings
5
6
  from django.db import transaction
7
+ from django.db.models import Max
6
8
  from django.db.models.query import QuerySet
7
9
  from django.utils import timezone
8
10
 
@@ -10,19 +12,21 @@ import abstract_block_dumper._internal.services.utils as abd_utils
10
12
  import abstract_block_dumper.models as abd_models
11
13
 
12
14
 
13
- def get_ready_to_retry_attempts() -> QuerySet[abd_models.TaskAttempt]:
14
- return abd_models.TaskAttempt.objects.filter(
15
- next_retry_at__isnull=False,
16
- next_retry_at__lte=timezone.now(),
17
- attempt_count__lt=abd_utils.get_max_attempt_limit(),
18
- ).exclude(
19
- status=abd_models.TaskAttempt.Status.SUCCESS,
15
+ def get_ready_to_retry_attempts() -> Iterator[abd_models.TaskAttempt]:
16
+ return (
17
+ abd_models.TaskAttempt.objects.filter(
18
+ next_retry_at__isnull=False,
19
+ next_retry_at__lte=timezone.now(),
20
+ attempt_count__lt=abd_utils.get_max_attempt_limit(),
21
+ )
22
+ .exclude(
23
+ status=abd_models.TaskAttempt.Status.SUCCESS,
24
+ )
25
+ .iterator()
20
26
  )
21
27
 
22
28
 
23
29
  def executed_block_numbers(executable_path: str, args_json: str, from_block: int, to_block: int) -> set[int]:
24
- # Use iterator() to avoid Django's QuerySet caching which causes memory leaks
25
- # during long-running backfill operations
26
30
  block_numbers = (
27
31
  abd_models.TaskAttempt.objects.filter(
28
32
  executable_path=executable_path,
@@ -151,7 +155,5 @@ def task_create_or_get_pending(
151
155
 
152
156
 
153
157
  def get_the_latest_executed_block_number() -> int | None:
154
- qs = abd_models.TaskAttempt.objects.order_by("-block_number").first()
155
- if qs:
156
- return qs.block_number
157
- return None
158
+ result = abd_models.TaskAttempt.objects.aggregate(max_block=Max("block_number"))
159
+ return result["max_block"]
@@ -0,0 +1,78 @@
1
+ import bittensor as bt
2
+ import structlog
3
+
4
+ import abstract_block_dumper._internal.services.utils as abd_utils
5
+
6
+ logger = structlog.get_logger(__name__)
7
+
8
+
9
+ # Blocks older than this threshold from current head require archive network
10
+ ARCHIVE_BLOCK_THRESHOLD = 300
11
+
12
+
13
+ class BittensorConnectionClient:
14
+ """
15
+ Manages connections to regular and archive Bittensor subtensor networks.
16
+ """
17
+
18
+ def __init__(self, network: str) -> None:
19
+ self.network = network
20
+ self._subtensor: bt.Subtensor | None = None
21
+ self._archive_subtensor: bt.Subtensor | None = None
22
+ self._current_block_cache: int | None = None
23
+
24
+ def get_for_block(self, block_number: int) -> bt.Subtensor:
25
+ """Get the appropriate subtensor client for the given block number."""
26
+ raise NotImplementedError
27
+
28
+ @property
29
+ def subtensor(self) -> bt.Subtensor:
30
+ """Get the regular subtensor connection, creating it if needed."""
31
+ if self._subtensor is None:
32
+ self._subtensor = abd_utils.get_bittensor_client(self.network)
33
+ return self._subtensor
34
+
35
+ @subtensor.setter
36
+ def subtensor(self, value: bt.Subtensor | None) -> None:
37
+ """Set or reset the subtensor connection."""
38
+ self._subtensor = value
39
+
40
+ @property
41
+ def archive_subtensor(self) -> bt.Subtensor:
42
+ """Get the archive subtensor connection, creating it if needed."""
43
+ if self._archive_subtensor is None:
44
+ self._archive_subtensor = abd_utils.get_bittensor_client("archive")
45
+ return self._archive_subtensor
46
+
47
+ @archive_subtensor.setter
48
+ def archive_subtensor(self, value: bt.Subtensor | None) -> None:
49
+ """Set or reset the archive subtensor connection."""
50
+ self._archive_subtensor = value
51
+
52
+ def get_subtensor_for_block(self, block_number: int) -> bt.Subtensor:
53
+ """
54
+ Get the appropriate subtensor for the given block number.
55
+
56
+ Uses archive network for blocks older than ARCHIVE_BLOCK_THRESHOLD
57
+ from the current head.
58
+ """
59
+ if self._current_block_cache is None:
60
+ self._current_block_cache = self.subtensor.get_current_block()
61
+
62
+ blocks_behind = self._current_block_cache - block_number
63
+
64
+ if blocks_behind > ARCHIVE_BLOCK_THRESHOLD:
65
+ logger.debug(
66
+ "Using archive network for old block",
67
+ block_number=block_number,
68
+ blocks_behind=blocks_behind,
69
+ )
70
+ return self.archive_subtensor
71
+ return self.subtensor
72
+
73
+ def refresh_connections(self) -> None:
74
+ """Reset all subtensor connections to force re-establishment."""
75
+ self._subtensor = None
76
+ self._archive_subtensor = None
77
+ self._current_block_cache = None
78
+ logger.info("Subtensor connections reset")
@@ -15,7 +15,7 @@ import structlog
15
15
 
16
16
  import abstract_block_dumper._internal.dal.django_dal as abd_dal
17
17
  import abstract_block_dumper._internal.services.utils as abd_utils
18
- from abstract_block_dumper._internal.services.block_processor import BlockProcessor, block_processor_factory
18
+ from abstract_block_dumper._internal.services.block_processor import BaseBlockProcessor, block_processor_factory
19
19
  from abstract_block_dumper._internal.services.metrics import (
20
20
  BlockProcessingTimer,
21
21
  increment_archive_network_usage,
@@ -59,7 +59,7 @@ class BackfillScheduler:
59
59
 
60
60
  def __init__(
61
61
  self,
62
- block_processor: BlockProcessor,
62
+ block_processor: BaseBlockProcessor,
63
63
  network: str,
64
64
  from_block: int,
65
65
  to_block: int,
@@ -1,4 +1,6 @@
1
+ import itertools
1
2
  import time
3
+ from typing import Protocol
2
4
 
3
5
  import structlog
4
6
  from django.db import transaction
@@ -12,6 +14,25 @@ from abstract_block_dumper.models import TaskAttempt
12
14
  logger = structlog.get_logger(__name__)
13
15
 
14
16
 
17
+ class BaseBlockProcessor(Protocol):
18
+ """Protocol defining the interface for block processors."""
19
+
20
+ executor: CeleryExecutor
21
+ registry: BaseRegistry
22
+
23
+ def process_block(self, block_number: int) -> None:
24
+ """Process a single block - executes registered tasks for this block only."""
25
+ ...
26
+
27
+ def process_registry_item(self, registry_item: RegistryItem, block_number: int) -> None:
28
+ """Process a single registry item for a given block."""
29
+ ...
30
+
31
+ def recover_failed_retries(self, poll_interval: int, batch_size: int | None = None) -> None:
32
+ """Recover failed tasks that are ready to be retried."""
33
+ ...
34
+
35
+
15
36
  class BlockProcessor:
16
37
  def __init__(self, executor: CeleryExecutor, registry: BaseRegistry) -> None:
17
38
  self.executor = executor
@@ -59,9 +80,9 @@ class BlockProcessor:
59
80
  retry_count = 0
60
81
  retry_attempts = abd_dal.get_ready_to_retry_attempts()
61
82
 
62
- # Apply batch size limit if specified
83
+ # Apply batch size limit if specified (use islice for iterator compatibility)
63
84
  if batch_size is not None:
64
- retry_attempts = retry_attempts[:batch_size]
85
+ retry_attempts = itertools.islice(retry_attempts, batch_size)
65
86
 
66
87
  for retry_attempt in retry_attempts:
67
88
  time.sleep(poll_interval)
@@ -147,7 +168,7 @@ class BlockProcessor:
147
168
  def block_processor_factory(
148
169
  executor: CeleryExecutor | None = None,
149
170
  registry: BaseRegistry | None = None,
150
- ) -> BlockProcessor:
171
+ ) -> BaseBlockProcessor:
151
172
  return BlockProcessor(
152
173
  executor=executor or CeleryExecutor(),
153
174
  registry=registry or task_registry,
@@ -0,0 +1,136 @@
1
+ import time
2
+ from typing import Protocol
3
+
4
+ import structlog
5
+ from django import db
6
+ from django.conf import settings
7
+
8
+ import abstract_block_dumper._internal.dal.django_dal as abd_dal
9
+ from abstract_block_dumper._internal.providers.bittensor_client import BittensorConnectionClient
10
+ from abstract_block_dumper._internal.services.block_processor import BaseBlockProcessor, block_processor_factory
11
+ from abstract_block_dumper._internal.services.metrics import (
12
+ BlockProcessingTimer,
13
+ increment_blocks_processed,
14
+ set_block_lag,
15
+ set_current_block,
16
+ set_registered_tasks,
17
+ )
18
+
19
+ # Refresh bittensor connections every N blocks to prevent memory leaks from internal caches
20
+ CONNECTION_REFRESH_INTERVAL = 1000
21
+
22
+ logger = structlog.get_logger(__name__)
23
+
24
+
25
+ class BlockStateResolver(Protocol):
26
+ """Protocol defining the interface for block state resolvers."""
27
+
28
+ def get_starting_block(self) -> int:
29
+ """Determine which block to start processing from."""
30
+ ...
31
+
32
+
33
+ class DefaultBlockStateResolver:
34
+ """Default implementation that reads from settings and database."""
35
+
36
+ def __init__(self, bittensor_client: BittensorConnectionClient) -> None:
37
+ self.bittensor_client = bittensor_client
38
+
39
+ def get_starting_block(self) -> int:
40
+ start_setting = getattr(settings, "BLOCK_DUMPER_START_FROM_BLOCK", None)
41
+ if start_setting == "current":
42
+ return self.bittensor_client.subtensor.get_current_block()
43
+ if isinstance(start_setting, int):
44
+ return start_setting
45
+ # Default: resume from DB or current
46
+ return abd_dal.get_the_latest_executed_block_number() or self.bittensor_client.subtensor.get_current_block()
47
+
48
+
49
+ class TaskScheduler:
50
+ def __init__(
51
+ self,
52
+ block_processor: BaseBlockProcessor,
53
+ bittensor_client: BittensorConnectionClient,
54
+ state_resolver: BlockStateResolver,
55
+ poll_interval: int,
56
+ ) -> None:
57
+ self.block_processor = block_processor
58
+ self.poll_interval = poll_interval
59
+ self.bittensor_client = bittensor_client
60
+ self.last_processed_block = state_resolver.get_starting_block()
61
+ self.is_running = False
62
+ self._blocks_since_refresh = 0
63
+
64
+ def start(self) -> None:
65
+ self.is_running = True
66
+
67
+ registered_tasks_count = len(self.block_processor.registry.get_functions())
68
+ set_registered_tasks(registered_tasks_count)
69
+
70
+ logger.info(
71
+ "TaskScheduler started",
72
+ last_processed_block=self.last_processed_block,
73
+ registry_functions=registered_tasks_count,
74
+ )
75
+
76
+ while self.is_running:
77
+ try:
78
+ current_block = self.bittensor_client.subtensor.get_current_block()
79
+
80
+ # Only process the current head block, skip if already processed
81
+ if current_block != self.last_processed_block:
82
+ with BlockProcessingTimer(mode="realtime"):
83
+ self.block_processor.process_block(current_block)
84
+
85
+ set_current_block("realtime", current_block)
86
+ increment_blocks_processed("realtime")
87
+ set_block_lag("realtime", 0) # Head-only mode has no lag
88
+ self.last_processed_block = current_block
89
+ self._blocks_since_refresh += 1
90
+
91
+ # Periodic memory cleanup
92
+ if self._blocks_since_refresh >= CONNECTION_REFRESH_INTERVAL:
93
+ self._perform_cleanup()
94
+
95
+ time.sleep(self.poll_interval)
96
+
97
+ except KeyboardInterrupt:
98
+ logger.info("TaskScheduler stopping due to KeyboardInterrupt.")
99
+ self.stop()
100
+ break
101
+ except Exception:
102
+ logger.exception("Error in TaskScheduler loop")
103
+ time.sleep(self.poll_interval)
104
+
105
+ def stop(self) -> None:
106
+ self.is_running = False
107
+ logger.info("TaskScheduler stopped.")
108
+
109
+ def _perform_cleanup(self) -> None:
110
+ """Perform periodic memory cleanup to prevent leaks in long-running processes."""
111
+ # Reset bittensor connections to clear internal caches
112
+ self.bittensor_client.refresh_connections()
113
+
114
+ # Clear Django's query log (only accumulates if DEBUG=True)
115
+ db.reset_queries()
116
+
117
+ self._blocks_since_refresh = 0
118
+ logger.debug("Memory cleanup performed", blocks_processed=CONNECTION_REFRESH_INTERVAL)
119
+
120
+
121
+ def task_scheduler_factory(network: str = "finney") -> TaskScheduler:
122
+ """
123
+ Factory for TaskScheduler.
124
+
125
+ Args:
126
+ network (str): Bittensor network name. Defaults to "finney"
127
+
128
+ """
129
+ bittensor_client = BittensorConnectionClient(network=network)
130
+ state_resolver = DefaultBlockStateResolver(bittensor_client=bittensor_client)
131
+ return TaskScheduler(
132
+ block_processor=block_processor_factory(),
133
+ poll_interval=getattr(settings, "BLOCK_DUMPER_POLL_INTERVAL", 5),
134
+ bittensor_client=bittensor_client,
135
+ state_resolver=state_resolver,
136
+ )
@@ -17,7 +17,7 @@ def get_bittensor_client(network: str = "finney") -> bt.Subtensor:
17
17
  doesn't change during runtime.
18
18
  """
19
19
  logger.info("Creating new bittensor client for network", network=network)
20
- return bt.subtensor(network=network)
20
+ return bt.Subtensor(network=network)
21
21
 
22
22
 
23
23
  def get_current_celery_task_id() -> str:
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.9'
32
- __version_tuple__ = version_tuple = (0, 0, 9)
31
+ __version__ = version = '0.1.1'
32
+ __version_tuple__ = version_tuple = (0, 1, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -74,7 +74,9 @@ def schedule_retry(task_attempt: TaskAttempt) -> None:
74
74
 
75
75
 
76
76
  def _celery_task_wrapper(
77
- func: Callable[..., Any], block_number: int, **kwargs: dict[str, Any]
77
+ func: Callable[..., Any],
78
+ block_number: int,
79
+ **kwargs: dict[str, Any],
78
80
  ) -> dict[str, Any] | None:
79
81
  executable_path = abd_utils.get_executable_path(func)
80
82
 
@@ -0,0 +1,115 @@
1
+ from collections.abc import Generator
2
+ from typing import Any, NoReturn
3
+
4
+ import django
5
+ import pytest
6
+ from celery import Celery
7
+ from django.conf import settings
8
+
9
+ from abstract_block_dumper._internal.dal.memory_registry import RegistryItem, task_registry
10
+ from abstract_block_dumper.v1.decorators import block_task
11
+
12
+ from .django_fixtures import * # noqa: F401, F403
13
+
14
+ # Ensure Django is set up
15
+ if not settings.configured:
16
+ django.setup()
17
+
18
+
19
+ class MockedBlockProcessor:
20
+ """Mock implementation of BaseBlockProcessor for testing."""
21
+
22
+ def __init__(self, executor: Any = None, registry: Any = None) -> None:
23
+ self.executor = executor
24
+ self.registry = registry or task_registry
25
+ self.processed_blocks: list[int] = []
26
+ self.processed_registry_items: list[tuple[RegistryItem, int]] = []
27
+ self.recover_failed_retries_calls: list[tuple[int, int | None]] = []
28
+
29
+ def process_block(self, block_number: int) -> None:
30
+ self.processed_blocks.append(block_number)
31
+
32
+ def process_registry_item(self, registry_item: RegistryItem, block_number: int) -> None:
33
+ self.processed_registry_items.append((registry_item, block_number))
34
+
35
+ def recover_failed_retries(self, poll_interval: int, batch_size: int | None = None) -> None:
36
+ self.recover_failed_retries_calls.append((poll_interval, batch_size))
37
+
38
+
39
+ class MockedSubtensor:
40
+ """Mock implementation of bt.Subtensor for testing."""
41
+
42
+ def __init__(self, current_block: int = 1000) -> None:
43
+ self._current_block = current_block
44
+
45
+ def get_current_block(self) -> int:
46
+ return self._current_block
47
+
48
+ def set_current_block(self, block: int) -> None:
49
+ self._current_block = block
50
+
51
+
52
+ @pytest.fixture(autouse=True)
53
+ def celery_test_app() -> Generator[Celery, Any, None]:
54
+ """Configure Celery for testing with eager mode."""
55
+ app = Celery("test_app")
56
+ app.config_from_object(settings, namespace="CELERY")
57
+ return app
58
+
59
+
60
+ def every_block_task_func(block_number: int):
61
+ """
62
+ Test function for every block execution.
63
+ """
64
+ return f"Processed block {block_number}"
65
+
66
+
67
+ def modulo_task_func(block_number: int, netuid: int):
68
+ """
69
+ Test function for modulo condition execution.
70
+ """
71
+ return f"Modulo task processed block {block_number} for netuid {netuid}"
72
+
73
+
74
+ def failing_task_func(block_number: int) -> NoReturn:
75
+ """
76
+ Test function that always fails.
77
+ """
78
+ raise ValueError("Test error")
79
+
80
+
81
+ @pytest.fixture
82
+ def setup_test_tasks():
83
+ # Register test tasks using decorators
84
+
85
+ # every block
86
+ block_task(condition=lambda bn: True)(every_block_task_func)
87
+
88
+ # every 5 blocks
89
+ block_task(condition=lambda bn, netuid: bn % 5 == 0, args=[{"netuid": 1}, {"netuid": 2}])(modulo_task_func)
90
+
91
+ yield
92
+
93
+
94
+ @pytest.fixture(autouse=True)
95
+ def cleanup_memory_registry():
96
+ task_registry.clear()
97
+
98
+ yield
99
+
100
+ task_registry.clear()
101
+
102
+
103
+ @pytest.fixture
104
+ def mock_block_processor() -> MockedBlockProcessor:
105
+ return MockedBlockProcessor()
106
+
107
+
108
+ @pytest.fixture
109
+ def mock_subtensor() -> MockedSubtensor:
110
+ return MockedSubtensor(current_block=1000)
111
+
112
+
113
+ @pytest.fixture
114
+ def mock_archive_subtensor() -> MockedSubtensor:
115
+ return MockedSubtensor(current_block=1000)
@@ -7,6 +7,7 @@ from django.utils import timezone
7
7
  import abstract_block_dumper._internal.dal.django_dal as abd_dal
8
8
  import abstract_block_dumper._internal.services.utils as abd_utils
9
9
  from abstract_block_dumper._internal.dal.memory_registry import task_registry
10
+ from abstract_block_dumper._internal.providers.bittensor_client import BittensorConnectionClient
10
11
  from abstract_block_dumper._internal.services.block_processor import block_processor_factory
11
12
  from abstract_block_dumper.models import TaskAttempt
12
13
  from abstract_block_dumper.v1.decorators import block_task
@@ -151,3 +152,36 @@ def test_retry_recover_mechanism():
151
152
  status=TaskAttempt.Status.SUCCESS,
152
153
  )
153
154
  assert qs.count() == len(recover_ids)
155
+
156
+
157
+ def test_bittensor_client_uses_archive_for_old_blocks(
158
+ mock_subtensor,
159
+ mock_archive_subtensor,
160
+ ) -> None:
161
+ """Test that BittensorConnectionClient uses archive_subtensor for blocks older than 300 from current head."""
162
+ current_block = 1000
163
+
164
+ client = BittensorConnectionClient(network="testnet")
165
+ client._subtensor = mock_subtensor
166
+ client._archive_subtensor = mock_archive_subtensor
167
+ client._current_block_cache = current_block
168
+
169
+ # For old block (400 behind), should return archive_subtensor
170
+ old_block = 600 # 400 blocks behind (> 300 threshold)
171
+ result = client.get_subtensor_for_block(old_block)
172
+ assert result is mock_archive_subtensor
173
+
174
+ # For recent block (within 300), should return regular subtensor
175
+ recent_block = 800 # 200 blocks behind (< 300 threshold)
176
+ result = client.get_subtensor_for_block(recent_block)
177
+ assert result is mock_subtensor
178
+
179
+ # For block exactly at threshold (300 behind), should return regular subtensor
180
+ threshold_block = 700 # exactly 300 blocks behind
181
+ result = client.get_subtensor_for_block(threshold_block)
182
+ assert result is mock_subtensor
183
+
184
+ # For block just over threshold (301 behind), should return archive_subtensor
185
+ just_over_threshold_block = 699 # 301 blocks behind
186
+ result = client.get_subtensor_for_block(just_over_threshold_block)
187
+ assert result is mock_archive_subtensor
@@ -1,195 +0,0 @@
1
- import time
2
-
3
- import bittensor as bt
4
- import structlog
5
- from django.conf import settings
6
-
7
- import abstract_block_dumper._internal.dal.django_dal as abd_dal
8
- import abstract_block_dumper._internal.services.utils as abd_utils
9
- from abstract_block_dumper._internal.services.block_processor import BlockProcessor, block_processor_factory
10
- from abstract_block_dumper._internal.services.metrics import (
11
- BlockProcessingTimer,
12
- increment_blocks_processed,
13
- set_block_lag,
14
- set_current_block,
15
- set_registered_tasks,
16
- )
17
-
18
- logger = structlog.get_logger(__name__)
19
-
20
- # Blocks older than this threshold from current head require archive network
21
- ARCHIVE_BLOCK_THRESHOLD = 300
22
-
23
-
24
- class TaskScheduler:
25
- def __init__(
26
- self,
27
- block_processor: BlockProcessor,
28
- network: str,
29
- poll_interval: int,
30
- realtime_head_only: bool = False,
31
- ) -> None:
32
- self.block_processor = block_processor
33
- self.network = network
34
- self.poll_interval = poll_interval
35
- self.realtime_head_only = realtime_head_only
36
- self.last_processed_block = -1
37
- self.is_running = False
38
- self._subtensor: bt.Subtensor | None = None
39
- self._archive_subtensor: bt.Subtensor | None = None
40
- self._current_block_cache: int | None = None
41
-
42
- @property
43
- def subtensor(self) -> bt.Subtensor:
44
- """Get the regular subtensor connection, creating it if needed."""
45
- if self._subtensor is None:
46
- self._subtensor = abd_utils.get_bittensor_client(self.network)
47
- return self._subtensor
48
-
49
- @subtensor.setter
50
- def subtensor(self, value: bt.Subtensor | None) -> None:
51
- """Set or reset the subtensor connection."""
52
- self._subtensor = value
53
-
54
- @property
55
- def archive_subtensor(self) -> bt.Subtensor:
56
- """Get the archive subtensor connection, creating it if needed."""
57
- if self._archive_subtensor is None:
58
- self._archive_subtensor = abd_utils.get_bittensor_client("archive")
59
- return self._archive_subtensor
60
-
61
- @archive_subtensor.setter
62
- def archive_subtensor(self, value: bt.Subtensor | None) -> None:
63
- """Set or reset the archive subtensor connection."""
64
- self._archive_subtensor = value
65
-
66
- def get_subtensor_for_block(self, block_number: int) -> bt.Subtensor:
67
- """
68
- Get the appropriate subtensor for the given block number.
69
-
70
- Uses archive network for blocks older than ARCHIVE_BLOCK_THRESHOLD
71
- from the current head.
72
- """
73
- if self._current_block_cache is None:
74
- self._current_block_cache = self.subtensor.get_current_block()
75
-
76
- blocks_behind = self._current_block_cache - block_number
77
-
78
- if blocks_behind > ARCHIVE_BLOCK_THRESHOLD:
79
- logger.debug(
80
- "Using archive network for old block",
81
- block_number=block_number,
82
- blocks_behind=blocks_behind,
83
- )
84
- return self.archive_subtensor
85
- return self.subtensor
86
-
87
- def refresh_connections(self) -> None:
88
- """Reset all subtensor connections to force re-establishment."""
89
- self._subtensor = None
90
- self._archive_subtensor = None
91
- self._current_block_cache = None
92
- logger.info("Subtensor connections reset")
93
-
94
- def start(self) -> None:
95
- self.is_running = True
96
-
97
- self.initialize_last_block()
98
-
99
- registered_tasks_count = len(self.block_processor.registry.get_functions())
100
- set_registered_tasks(registered_tasks_count)
101
-
102
- logger.info(
103
- "TaskScheduler started",
104
- last_processed_block=self.last_processed_block,
105
- registry_functions=registered_tasks_count,
106
- )
107
-
108
- while self.is_running:
109
- try:
110
- if self._current_block_cache is not None:
111
- self.subtensor = self.get_subtensor_for_block(self._current_block_cache)
112
-
113
- # Update current block cache for archive network decision
114
- self._current_block_cache = self.subtensor.get_current_block()
115
- current_block = self._current_block_cache
116
-
117
- if self.realtime_head_only:
118
- # Only process the current head block, skip if already processed
119
- if current_block != self.last_processed_block:
120
- with BlockProcessingTimer(mode="realtime"):
121
- self.block_processor.process_block(current_block)
122
-
123
- set_current_block("realtime", current_block)
124
- increment_blocks_processed("realtime")
125
- set_block_lag("realtime", 0) # Head-only mode has no lag
126
- self.last_processed_block = current_block
127
-
128
- time.sleep(self.poll_interval)
129
- else:
130
- # Original behavior: process all blocks from last_processed to current
131
- for block_number in range(self.last_processed_block + 1, current_block + 1):
132
- with BlockProcessingTimer(mode="realtime"):
133
- self.block_processor.process_block(block_number)
134
-
135
- # Update metrics
136
- set_current_block("realtime", block_number)
137
- increment_blocks_processed("realtime")
138
- set_block_lag("realtime", current_block - block_number)
139
-
140
- time.sleep(self.poll_interval)
141
- self.last_processed_block = block_number
142
-
143
- except KeyboardInterrupt:
144
- logger.info("TaskScheduler stopping due to KeyboardInterrupt.")
145
- self.stop()
146
- break
147
- except Exception:
148
- logger.error("Fatal scheduler error", exc_info=True)
149
- # resume the loop even if task failed
150
- time.sleep(self.poll_interval)
151
-
152
- def stop(self) -> None:
153
- self.is_running = False
154
- logger.info("TaskScheduler stopped.")
155
-
156
- def initialize_last_block(self) -> None:
157
- # Safe getattr in case setting is not defined
158
- start_from_block_setting = getattr(settings, "BLOCK_DUMPER_START_FROM_BLOCK", None)
159
-
160
- if start_from_block_setting is not None:
161
- if start_from_block_setting == "current":
162
- self.last_processed_block = self.subtensor.get_current_block()
163
- logger.info("Starting from current blockchain block", block_number=self.last_processed_block)
164
-
165
- elif isinstance(start_from_block_setting, int):
166
- self.last_processed_block = start_from_block_setting
167
- logger.info("Starting from configured block", block_number=self.last_processed_block)
168
- else:
169
- error_msg = f"Invalid BLOCK_DUMPER_START_FROM_BLOCK value: {start_from_block_setting}"
170
- raise ValueError(error_msg)
171
- else:
172
- # Default behavior - resume from database
173
- last_block_number = abd_dal.get_the_latest_executed_block_number()
174
-
175
- self.last_processed_block = last_block_number or self.subtensor.get_current_block()
176
- logger.info(
177
- "Resume from the last database block or start from the current block",
178
- last_processed_block=self.last_processed_block,
179
- )
180
-
181
-
182
- def task_scheduler_factory(network: str = "finney") -> TaskScheduler:
183
- """
184
- Factory for TaskScheduler.
185
-
186
- Args:
187
- network (str): Bittensor network name. Defaults to "finney"
188
-
189
- """
190
- return TaskScheduler(
191
- block_processor=block_processor_factory(),
192
- network=network,
193
- poll_interval=getattr(settings, "BLOCK_DUMPER_POLL_INTERVAL", 1),
194
- realtime_head_only=getattr(settings, "BLOCK_DUMPER_REALTIME_HEAD_ONLY", True),
195
- )
@@ -1,65 +0,0 @@
1
- import django
2
- import pytest
3
- from celery import Celery
4
- from django.conf import settings
5
-
6
- from abstract_block_dumper._internal.dal.memory_registry import task_registry
7
-
8
- from .django_fixtures import * # noqa: F401, F403
9
-
10
- # Ensure Django is set up
11
- if not settings.configured:
12
- django.setup()
13
-
14
-
15
- @pytest.fixture(autouse=True)
16
- def celery_test_app():
17
- """Configure Celery for testing with eager mode."""
18
- app = Celery("test_app")
19
- app.config_from_object(settings, namespace="CELERY")
20
-
21
- yield app
22
-
23
-
24
- def every_block_task_func(block_number: int):
25
- """
26
- Test function for every block execution.
27
- """
28
- return f"Processed block {block_number}"
29
-
30
-
31
- def modulo_task_func(block_number: int, netuid: int):
32
- """
33
- Test function for modulo condition execution.
34
- """
35
- return f"Modulo task processed block {block_number} for netuid {netuid}"
36
-
37
-
38
- def failing_task_func(block_number: int):
39
- """
40
- Test function that always fails.
41
- """
42
- raise ValueError("Test error")
43
-
44
-
45
- @pytest.fixture
46
- def setup_test_tasks():
47
- # Register test tasks using decorators
48
- from abstract_block_dumper.v1.decorators import block_task
49
-
50
- # every block
51
- block_task(condition=lambda bn: True)(every_block_task_func)
52
-
53
- # every 5 blocks
54
- block_task(condition=lambda bn, netuid: bn % 5 == 0, args=[{"netuid": 1}, {"netuid": 2}])(modulo_task_func)
55
-
56
- yield
57
-
58
-
59
- @pytest.fixture(autouse=True)
60
- def cleanup_memory_registry():
61
- task_registry.clear()
62
-
63
- yield
64
-
65
- task_registry.clear()