golem-vm-provider 0.1.24__py3-none-any.whl → 0.1.27__py3-none-any.whl
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.
- {golem_vm_provider-0.1.24.dist-info → golem_vm_provider-0.1.27.dist-info}/METADATA +17 -10
- golem_vm_provider-0.1.27.dist-info/RECORD +38 -0
- {golem_vm_provider-0.1.24.dist-info → golem_vm_provider-0.1.27.dist-info}/WHEEL +1 -1
- golem_vm_provider-0.1.27.dist-info/entry_points.txt +4 -0
- provider/api/models.py +10 -10
- provider/api/routes.py +89 -95
- provider/config.py +101 -21
- provider/container.py +84 -0
- provider/discovery/__init__.py +8 -2
- provider/discovery/advertiser.py +41 -63
- provider/discovery/golem_base_advertiser.py +135 -0
- provider/discovery/golem_base_utils.py +10 -0
- provider/discovery/resource_monitor.py +34 -0
- provider/discovery/resource_tracker.py +1 -1
- provider/discovery/service.py +24 -0
- provider/main.py +88 -171
- provider/security/ethereum.py +9 -13
- provider/security/faucet.py +132 -0
- provider/service.py +67 -0
- provider/utils/__init__.py +0 -0
- provider/utils/logging.py +11 -27
- provider/utils/port_display.py +27 -1
- provider/utils/retry.py +39 -0
- provider/vm/__init__.py +1 -1
- provider/vm/models.py +13 -12
- provider/vm/multipass.py +2 -416
- provider/vm/multipass_adapter.py +221 -0
- provider/vm/name_mapper.py +5 -5
- provider/vm/port_manager.py +67 -26
- provider/vm/provider.py +48 -0
- provider/vm/proxy_manager.py +4 -3
- provider/vm/service.py +91 -0
- golem_vm_provider-0.1.24.dist-info/RECORD +0 -27
- golem_vm_provider-0.1.24.dist-info/entry_points.txt +0 -3
provider/container.py
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
import os
|
2
|
+
from dependency_injector import containers, providers
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from .config import settings
|
6
|
+
from .discovery.resource_tracker import ResourceTracker
|
7
|
+
from .discovery.golem_base_advertiser import GolemBaseAdvertiser
|
8
|
+
from .discovery.advertiser import DiscoveryServerAdvertiser
|
9
|
+
from .discovery.service import AdvertisementService
|
10
|
+
from .service import ProviderService
|
11
|
+
from .vm.multipass_adapter import MultipassAdapter
|
12
|
+
from .vm.service import VMService
|
13
|
+
from .vm.name_mapper import VMNameMapper
|
14
|
+
from .vm.port_manager import PortManager
|
15
|
+
from .vm.proxy_manager import PythonProxyManager
|
16
|
+
|
17
|
+
|
18
|
+
class Container(containers.DeclarativeContainer):
|
19
|
+
"""Dependency injection container."""
|
20
|
+
|
21
|
+
config = providers.Configuration()
|
22
|
+
|
23
|
+
resource_tracker = providers.Singleton(ResourceTracker)
|
24
|
+
|
25
|
+
advertiser = providers.Selector(
|
26
|
+
config.ADVERTISER_TYPE,
|
27
|
+
golem_base=providers.Singleton(
|
28
|
+
GolemBaseAdvertiser,
|
29
|
+
resource_tracker=resource_tracker,
|
30
|
+
),
|
31
|
+
discovery_server=providers.Singleton(
|
32
|
+
DiscoveryServerAdvertiser,
|
33
|
+
resource_tracker=resource_tracker,
|
34
|
+
),
|
35
|
+
)
|
36
|
+
|
37
|
+
advertisement_service = providers.Singleton(
|
38
|
+
AdvertisementService,
|
39
|
+
advertiser=advertiser,
|
40
|
+
)
|
41
|
+
|
42
|
+
vm_name_mapper = providers.Singleton(
|
43
|
+
VMNameMapper,
|
44
|
+
db_path=Path(settings.VM_DATA_DIR) / "vm_names.json",
|
45
|
+
)
|
46
|
+
|
47
|
+
port_manager = providers.Singleton(
|
48
|
+
PortManager,
|
49
|
+
start_port=config.PORT_RANGE_START,
|
50
|
+
end_port=config.PORT_RANGE_END,
|
51
|
+
state_file=providers.Callable(
|
52
|
+
os.path.join,
|
53
|
+
config.PROXY_STATE_DIR,
|
54
|
+
"ports.json"
|
55
|
+
),
|
56
|
+
discovery_port=config.PORT,
|
57
|
+
skip_verification=config.SKIP_PORT_VERIFICATION,
|
58
|
+
)
|
59
|
+
|
60
|
+
proxy_manager = providers.Singleton(
|
61
|
+
PythonProxyManager,
|
62
|
+
port_manager=port_manager,
|
63
|
+
name_mapper=vm_name_mapper,
|
64
|
+
)
|
65
|
+
|
66
|
+
vm_provider = providers.Singleton(
|
67
|
+
MultipassAdapter,
|
68
|
+
proxy_manager=proxy_manager,
|
69
|
+
name_mapper=vm_name_mapper,
|
70
|
+
)
|
71
|
+
|
72
|
+
vm_service = providers.Singleton(
|
73
|
+
VMService,
|
74
|
+
provider=vm_provider,
|
75
|
+
resource_tracker=resource_tracker,
|
76
|
+
name_mapper=vm_name_mapper,
|
77
|
+
)
|
78
|
+
|
79
|
+
provider_service = providers.Singleton(
|
80
|
+
ProviderService,
|
81
|
+
vm_service=vm_service,
|
82
|
+
advertisement_service=advertisement_service,
|
83
|
+
port_manager=port_manager,
|
84
|
+
)
|
provider/discovery/__init__.py
CHANGED
@@ -1,6 +1,12 @@
|
|
1
|
-
from .advertiser import
|
1
|
+
from .advertiser import Advertiser, DiscoveryServerAdvertiser
|
2
|
+
from .golem_base_advertiser import GolemBaseAdvertiser
|
3
|
+
from .resource_monitor import ResourceMonitor
|
4
|
+
from .service import AdvertisementService
|
2
5
|
|
3
6
|
__all__ = [
|
7
|
+
"Advertiser",
|
8
|
+
"DiscoveryServerAdvertiser",
|
9
|
+
"GolemBaseAdvertiser",
|
4
10
|
"ResourceMonitor",
|
5
|
-
"
|
11
|
+
"AdvertisementService",
|
6
12
|
]
|
provider/discovery/advertiser.py
CHANGED
@@ -1,90 +1,71 @@
|
|
1
1
|
import aiohttp
|
2
2
|
import asyncio
|
3
3
|
import logging
|
4
|
-
import
|
5
|
-
from
|
6
|
-
from typing import Dict, Optional
|
4
|
+
from abc import ABC, abstractmethod
|
5
|
+
from typing import Optional
|
7
6
|
|
8
7
|
from ..config import settings
|
9
8
|
from ..utils.retry import async_retry
|
9
|
+
from .resource_tracker import ResourceTracker
|
10
10
|
|
11
11
|
logger = logging.getLogger(__name__)
|
12
12
|
|
13
|
-
class
|
14
|
-
"""
|
15
|
-
|
16
|
-
@
|
17
|
-
def
|
18
|
-
"""
|
19
|
-
|
20
|
-
|
21
|
-
@
|
22
|
-
def
|
23
|
-
"""
|
24
|
-
|
25
|
-
|
26
|
-
@
|
27
|
-
def
|
28
|
-
"""
|
29
|
-
|
30
|
-
|
31
|
-
@
|
32
|
-
def
|
33
|
-
"""
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
"""Get memory usage percentage."""
|
39
|
-
return psutil.virtual_memory().percent
|
40
|
-
|
41
|
-
@staticmethod
|
42
|
-
def get_storage_percent() -> float:
|
43
|
-
"""Get storage usage percentage."""
|
44
|
-
return psutil.disk_usage("/").percent
|
45
|
-
|
46
|
-
class ResourceAdvertiser:
|
47
|
-
"""Advertise available resources to discovery service."""
|
13
|
+
class Advertiser(ABC):
|
14
|
+
"""Abstract base class for advertisers."""
|
15
|
+
|
16
|
+
@abstractmethod
|
17
|
+
async def initialize(self):
|
18
|
+
"""Initialize the advertiser."""
|
19
|
+
pass
|
20
|
+
|
21
|
+
@abstractmethod
|
22
|
+
async def start_loop(self):
|
23
|
+
"""Start the advertising loop."""
|
24
|
+
pass
|
25
|
+
|
26
|
+
@abstractmethod
|
27
|
+
async def stop(self):
|
28
|
+
"""Stop the advertising loop."""
|
29
|
+
pass
|
30
|
+
|
31
|
+
@abstractmethod
|
32
|
+
async def post_advertisement(self):
|
33
|
+
"""Post a single advertisement."""
|
34
|
+
pass
|
35
|
+
|
36
|
+
class DiscoveryServerAdvertiser(Advertiser):
|
37
|
+
"""Advertise available resources to a discovery service."""
|
48
38
|
|
49
39
|
def __init__(
|
50
40
|
self,
|
51
41
|
resource_tracker: 'ResourceTracker',
|
52
42
|
discovery_url: Optional[str] = None,
|
53
43
|
provider_id: Optional[str] = None,
|
54
|
-
update_interval: Optional[int] = None
|
55
44
|
):
|
56
45
|
self.resource_tracker = resource_tracker
|
57
46
|
self.discovery_url = discovery_url or settings.DISCOVERY_URL
|
58
47
|
self.provider_id = provider_id or settings.PROVIDER_ID
|
59
|
-
self.update_interval = update_interval or settings.ADVERTISEMENT_INTERVAL
|
60
48
|
self.session: Optional[aiohttp.ClientSession] = None
|
61
49
|
self._stop_event = asyncio.Event()
|
62
50
|
|
63
|
-
async def
|
64
|
-
"""
|
51
|
+
async def initialize(self):
|
52
|
+
"""Initialize the advertiser."""
|
65
53
|
self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
# Test discovery service connection with retries
|
54
|
+
self.resource_tracker.on_update(
|
55
|
+
lambda: asyncio.create_task(self.post_advertisement())
|
56
|
+
)
|
70
57
|
try:
|
71
58
|
await self._check_discovery_health()
|
72
59
|
except Exception as e:
|
73
60
|
logger.warning(f"Could not connect to discovery service after retries, continuing without advertising: {e}")
|
74
61
|
return
|
75
|
-
|
62
|
+
|
63
|
+
async def start_loop(self):
|
64
|
+
"""Start advertising resources in a loop."""
|
76
65
|
try:
|
77
66
|
while not self._stop_event.is_set():
|
78
|
-
|
79
|
-
|
80
|
-
except aiohttp.ClientError as e:
|
81
|
-
logger.error(f"Network error posting advertisement: {e}")
|
82
|
-
await asyncio.sleep(min(60, self.update_interval))
|
83
|
-
except Exception as e:
|
84
|
-
logger.error(f"Failed to post advertisement: {e}")
|
85
|
-
await asyncio.sleep(min(60, self.update_interval))
|
86
|
-
else:
|
87
|
-
await asyncio.sleep(self.update_interval)
|
67
|
+
await self.post_advertisement()
|
68
|
+
await asyncio.sleep(settings.ADVERTISEMENT_INTERVAL)
|
88
69
|
finally:
|
89
70
|
await self.stop()
|
90
71
|
|
@@ -106,19 +87,17 @@ class ResourceAdvertiser:
|
|
106
87
|
raise Exception(f"Discovery service health check failed: {response.status}")
|
107
88
|
|
108
89
|
@async_retry(retries=3, delay=1.0, backoff=2.0, exceptions=(aiohttp.ClientError, asyncio.TimeoutError))
|
109
|
-
async def
|
90
|
+
async def post_advertisement(self):
|
110
91
|
"""Post resource advertisement to discovery service."""
|
111
92
|
if not self.session:
|
112
93
|
raise RuntimeError("Session not initialized")
|
113
94
|
|
114
95
|
resources = self.resource_tracker.get_available_resources()
|
115
96
|
|
116
|
-
# Don't advertise if resources are too low
|
117
97
|
if not self.resource_tracker._meets_minimum_requirements(resources):
|
118
98
|
logger.warning("Resources too low, skipping advertisement")
|
119
99
|
return
|
120
100
|
|
121
|
-
# Get public IP with retries
|
122
101
|
try:
|
123
102
|
ip_address = await self._get_public_ip()
|
124
103
|
except Exception as e:
|
@@ -130,7 +109,7 @@ class ResourceAdvertiser:
|
|
130
109
|
f"{self.discovery_url}/api/v1/advertisements",
|
131
110
|
headers={
|
132
111
|
"X-Provider-ID": self.provider_id,
|
133
|
-
"X-Provider-Signature": "signature",
|
112
|
+
"X-Provider-Signature": "signature",
|
134
113
|
"Content-Type": "application/json"
|
135
114
|
},
|
136
115
|
json={
|
@@ -138,7 +117,7 @@ class ResourceAdvertiser:
|
|
138
117
|
"country": settings.PROVIDER_COUNTRY,
|
139
118
|
"resources": resources
|
140
119
|
},
|
141
|
-
timeout=aiohttp.ClientTimeout(total=5)
|
120
|
+
timeout=aiohttp.ClientTimeout(total=5)
|
142
121
|
) as response:
|
143
122
|
if not response.ok:
|
144
123
|
error_text = await response.text()
|
@@ -159,7 +138,6 @@ class ResourceAdvertiser:
|
|
159
138
|
if not self.session:
|
160
139
|
raise RuntimeError("Session not initialized")
|
161
140
|
|
162
|
-
# Try multiple IP services in case one fails
|
163
141
|
services = [
|
164
142
|
"https://api.ipify.org",
|
165
143
|
"https://ifconfig.me/ip",
|
@@ -0,0 +1,135 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from golem_base_sdk import GolemBaseClient, GolemBaseCreate, GolemBaseUpdate, GolemBaseDelete, Annotation
|
5
|
+
from .advertiser import Advertiser
|
6
|
+
from .golem_base_utils import get_provider_entity_keys
|
7
|
+
from ..config import settings
|
8
|
+
from ..utils.logging import setup_logger
|
9
|
+
|
10
|
+
logger = setup_logger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class GolemBaseAdvertiser(Advertiser):
|
14
|
+
"""Advertise available resources to the Golem Base network."""
|
15
|
+
|
16
|
+
def __init__(self, resource_tracker: "ResourceTracker"):
|
17
|
+
self.resource_tracker = resource_tracker
|
18
|
+
self.client: Optional[GolemBaseClient] = None
|
19
|
+
self._stop_event = asyncio.Event()
|
20
|
+
|
21
|
+
async def initialize(self):
|
22
|
+
"""Initialize the advertiser."""
|
23
|
+
private_key_hex = settings.ETHEREUM_PRIVATE_KEY.replace("0x", "")
|
24
|
+
private_key_bytes = bytes.fromhex(private_key_hex)
|
25
|
+
self.client = await GolemBaseClient.create(
|
26
|
+
rpc_url=settings.GOLEM_BASE_RPC_URL,
|
27
|
+
ws_url=settings.GOLEM_BASE_WS_URL,
|
28
|
+
private_key=private_key_bytes,
|
29
|
+
)
|
30
|
+
self.resource_tracker.on_update(
|
31
|
+
lambda: asyncio.create_task(self.post_advertisement())
|
32
|
+
)
|
33
|
+
|
34
|
+
async def start_loop(self):
|
35
|
+
"""Start advertising resources in a loop."""
|
36
|
+
try:
|
37
|
+
while not self._stop_event.is_set():
|
38
|
+
await self.post_advertisement()
|
39
|
+
await asyncio.sleep(settings.ADVERTISEMENT_INTERVAL)
|
40
|
+
finally:
|
41
|
+
await self.stop()
|
42
|
+
|
43
|
+
async def stop(self):
|
44
|
+
"""Stop advertising resources."""
|
45
|
+
self._stop_event.set()
|
46
|
+
if self.client:
|
47
|
+
await self.client.disconnect()
|
48
|
+
|
49
|
+
async def post_advertisement(self):
|
50
|
+
"""Post or update resource advertisement on the Golem Base network."""
|
51
|
+
if not self.client:
|
52
|
+
raise RuntimeError("Golem Base client not initialized")
|
53
|
+
|
54
|
+
resources = self.resource_tracker.get_available_resources()
|
55
|
+
if not self.resource_tracker._meets_minimum_requirements(resources):
|
56
|
+
logger.warning("Resources too low, skipping advertisement")
|
57
|
+
return
|
58
|
+
|
59
|
+
ip_address = settings.PUBLIC_IP
|
60
|
+
if not ip_address:
|
61
|
+
logger.error("Could not get public IP, skipping advertisement")
|
62
|
+
return
|
63
|
+
|
64
|
+
try:
|
65
|
+
existing_keys = await get_provider_entity_keys(self.client, settings.PROVIDER_ID)
|
66
|
+
|
67
|
+
string_annotations = [
|
68
|
+
Annotation(key="golem_type", value="provider"),
|
69
|
+
Annotation(key="golem_provider_id", value=settings.PROVIDER_ID),
|
70
|
+
Annotation(key="golem_ip_address", value=ip_address),
|
71
|
+
Annotation(key="golem_country", value=settings.PROVIDER_COUNTRY),
|
72
|
+
Annotation(key="golem_provider_name", value=settings.PROVIDER_NAME),
|
73
|
+
]
|
74
|
+
numeric_annotations = [
|
75
|
+
Annotation(key="golem_cpu", value=resources["cpu"]),
|
76
|
+
Annotation(key="golem_memory", value=resources["memory"]),
|
77
|
+
Annotation(key="golem_storage", value=resources["storage"]),
|
78
|
+
]
|
79
|
+
|
80
|
+
if len(existing_keys) > 1:
|
81
|
+
logger.warning(f"Found {len(existing_keys)} advertisements. Cleaning up and creating a new one.")
|
82
|
+
deletes = [GolemBaseDelete(entity_key=key) for key in existing_keys]
|
83
|
+
await self.client.delete_entities(deletes)
|
84
|
+
await self._create_advertisement(string_annotations, numeric_annotations)
|
85
|
+
|
86
|
+
elif len(existing_keys) == 1:
|
87
|
+
entity_key = existing_keys[0]
|
88
|
+
metadata = await self.client.get_entity_metadata(entity_key)
|
89
|
+
|
90
|
+
current_annotations = {ann.key: ann.value for ann in metadata.numeric_annotations}
|
91
|
+
current_annotations.update({ann.key: ann.value for ann in metadata.string_annotations})
|
92
|
+
|
93
|
+
# Full comparison of all annotations
|
94
|
+
expected_annotations = {ann.key: ann.value for ann in string_annotations}
|
95
|
+
expected_annotations.update({ann.key: ann.value for ann in numeric_annotations})
|
96
|
+
|
97
|
+
# Debugging logs to compare annotations
|
98
|
+
logger.info(f"IP address from settings: {ip_address}")
|
99
|
+
logger.info(f"Current on-chain annotations: {current_annotations}")
|
100
|
+
logger.info(f"Expected annotations based on current config: {expected_annotations}")
|
101
|
+
|
102
|
+
if sorted(current_annotations.items()) == sorted(expected_annotations.items()):
|
103
|
+
logger.info("Advertisement is up-to-date. Waiting for expiration.")
|
104
|
+
else:
|
105
|
+
logger.info("Advertisement is outdated. Updating.")
|
106
|
+
update = GolemBaseUpdate(
|
107
|
+
entity_key=entity_key,
|
108
|
+
data=b"",
|
109
|
+
btl=settings.ADVERTISEMENT_INTERVAL * 2,
|
110
|
+
string_annotations=string_annotations,
|
111
|
+
numeric_annotations=numeric_annotations,
|
112
|
+
)
|
113
|
+
await self.client.update_entities([update])
|
114
|
+
logger.info(f"Updated advertisement. Entity key: {entity_key}")
|
115
|
+
|
116
|
+
else: # No existing keys
|
117
|
+
await self._create_advertisement(string_annotations, numeric_annotations)
|
118
|
+
|
119
|
+
except Exception as e:
|
120
|
+
logger.error(f"Failed to post or update advertisement on Golem Base: {e}")
|
121
|
+
|
122
|
+
async def _create_advertisement(self, string_annotations, numeric_annotations):
|
123
|
+
"""Helper to create a new advertisement."""
|
124
|
+
entity = GolemBaseCreate(
|
125
|
+
data=b"",
|
126
|
+
btl=settings.ADVERTISEMENT_INTERVAL * 2,
|
127
|
+
string_annotations=string_annotations,
|
128
|
+
numeric_annotations=numeric_annotations,
|
129
|
+
)
|
130
|
+
receipts = await self.client.create_entities([entity])
|
131
|
+
if receipts:
|
132
|
+
receipt = receipts[0]
|
133
|
+
logger.info(f"Posted new advertisement. Entity key: {receipt.entity_key}")
|
134
|
+
else:
|
135
|
+
logger.error("Failed to post advertisement: no receipt received")
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from typing import List
|
2
|
+
from golem_base_sdk import GolemBaseClient, EntityKey, QueryEntitiesResult, GenericBytes
|
3
|
+
from ..config import settings
|
4
|
+
|
5
|
+
async def get_provider_entity_keys(client: GolemBaseClient, provider_id: str) -> List[EntityKey]:
|
6
|
+
"""Get all entity keys for a given provider ID."""
|
7
|
+
query = f'golem_provider_id="{provider_id}"'
|
8
|
+
results: list[QueryEntitiesResult] = await client.query_entities(query)
|
9
|
+
# The entity_key from query_entities is a hex string, convert it to an EntityKey object
|
10
|
+
return [EntityKey(GenericBytes.from_hex_string(result.entity_key)) for result in results]
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import psutil
|
2
|
+
|
3
|
+
class ResourceMonitor:
|
4
|
+
"""Monitor system resources."""
|
5
|
+
|
6
|
+
@staticmethod
|
7
|
+
def get_cpu_count() -> int:
|
8
|
+
"""Get number of CPU cores."""
|
9
|
+
return psutil.cpu_count()
|
10
|
+
|
11
|
+
@staticmethod
|
12
|
+
def get_memory_gb() -> int:
|
13
|
+
"""Get available memory in GB."""
|
14
|
+
return psutil.virtual_memory().available // (1024 ** 3)
|
15
|
+
|
16
|
+
@staticmethod
|
17
|
+
def get_storage_gb() -> int:
|
18
|
+
"""Get available storage in GB."""
|
19
|
+
return psutil.disk_usage("/").free // (1024 ** 3)
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
def get_cpu_percent() -> float:
|
23
|
+
"""Get CPU usage percentage."""
|
24
|
+
return psutil.cpu_percent(interval=1)
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def get_memory_percent() -> float:
|
28
|
+
"""Get memory usage percentage."""
|
29
|
+
return psutil.virtual_memory().percent
|
30
|
+
|
31
|
+
@staticmethod
|
32
|
+
def get_storage_percent() -> float:
|
33
|
+
"""Get storage usage percentage."""
|
34
|
+
return psutil.disk_usage("/").percent
|
@@ -11,7 +11,7 @@ class ResourceTracker:
|
|
11
11
|
|
12
12
|
def __init__(self):
|
13
13
|
"""Initialize resource tracker."""
|
14
|
-
from .
|
14
|
+
from .resource_monitor import ResourceMonitor
|
15
15
|
self.total_resources = {
|
16
16
|
"cpu": ResourceMonitor.get_cpu_count(),
|
17
17
|
"memory": ResourceMonitor.get_memory_gb(),
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from .advertiser import Advertiser
|
5
|
+
from ..config import settings
|
6
|
+
|
7
|
+
class AdvertisementService:
|
8
|
+
"""Service for managing the advertisement lifecycle."""
|
9
|
+
|
10
|
+
def __init__(self, advertiser: Advertiser):
|
11
|
+
self.advertiser = advertiser
|
12
|
+
self._task: Optional[asyncio.Task] = None
|
13
|
+
|
14
|
+
async def start(self):
|
15
|
+
"""Initialize and start the advertiser."""
|
16
|
+
await self.advertiser.initialize()
|
17
|
+
self._task = asyncio.create_task(self.advertiser.start_loop())
|
18
|
+
|
19
|
+
async def stop(self):
|
20
|
+
"""Stop the advertiser."""
|
21
|
+
if self._task:
|
22
|
+
self._task.cancel()
|
23
|
+
await self._task
|
24
|
+
await self.advertiser.stop()
|