-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implementing logic #7
Implementing logic #7
Conversation
2275179
to
0dbf21f
Compare
bt_ddos_shield/event_processor.py
Outdated
print(f"MinerShieldEvent: {event.event_description}\nException happened:") | ||
print(traceback.format_exc()) | ||
else: | ||
print(f"MinerShieldEvent: {event.event_description}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This prints, not logs, but I also think it is generally assembled wrong and won't work. No worries, you'll fix it when you start running it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is class mostly (and maybe only) for unit tests. But I change name to PrintingMinerShieldEventProcessor, because probably in the future there will be LoggingMinerShieldEventProcessor. Also it works - just run unit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And if we ever print that, we'd rather use stderr.
traceback.print_exc()
can be a sufficient routine for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed it already in future commits. But now I'm printing exception captured inside MinerShieldEvent. Probably not, but there is possibility, that it wouldn't be last exception thrown and print_exc calls sys.exception() inside to get the last one.
bt_ddos_shield/miner_shield.py
Outdated
|
||
Args: | ||
validator_hotkey: The hotkey of the validator. | ||
""" | ||
self._add_task(MinerShieldBanValidatorTask(validator_hotkey)) | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pass |
bt_ddos_shield/miner_shield.py
Outdated
def _add_task(self, task): | ||
""" | ||
Add task to task queue. It will be handled by _worker_function. | ||
""" | ||
if not isinstance(task, MinerShieldTask): | ||
raise Exception("Task is not instance of MinerShieldTask") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def _add_task(self, task): | |
""" | |
Add task to task queue. It will be handled by _worker_function. | |
""" | |
if not isinstance(task, MinerShieldTask): | |
raise Exception("Task is not instance of MinerShieldTask") | |
def _add_task(self, task: MinerShieldTask): | |
""" | |
Add task to task queue. It will be handled by _worker_function. | |
""" |
Traditionally we don't implement type enforcement in the first lines of every method and function. Rather than running it in runtime, we have external tools for that which perform static analysis (whenever possible).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EncryptionManager was written using this check also in code - so I made this the same. And I forgot to specify type in parameter. Fixed - using trick with string literal, because there are no forward declarations in Python.
bt_ddos_shield/miner_shield.py
Outdated
if not isinstance(task, MinerShieldTask): | ||
raise Exception("Task is not instance of MinerShieldTask") | ||
if not self.run: | ||
raise Exception("Shield is disabled") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to raise any exceptions you'll need to
class ShieldException(Exception):
pass
class ShieldIsDisabled(ShieldException):
pass
and raise that - this way the user of the module will be able to catch the intended exceptions without catching every exception, which includes KeyError, ValueError etc
However, in this particular case you should raise a built-in ProgrammingError
, which is a builtin for cases like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exceptions added. And I added MinerShieldDisabledException, because _add_task is called at least from ban_validator and user can try to call it after stopping or before starting shield for some reason. Thats why I think MinerShieldDisabledException should be specified.
Except from above, I don't find anything like ProgrammingError (or ProgrammingException) - there is such class but in sqllite3 module.
bt_ddos_shield/miner_shield.py
Outdated
try_count = 1 | ||
|
||
while self.run: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
try_count = 1 | |
while self.run: | |
try_count: int = 0 | |
while try_count <= self.options.max_retries: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want tasks to be retried forever and I think this should be default behaviour - we can eventually talk about this. But I added new option to allow limited retrying.
bt_ddos_shield/miner_shield.py
Outdated
Initialize shield. Load state and initial validators set. | ||
""" | ||
self.state_manager.get_state() | ||
self.event_processor.add_event(MinerShieldEvent("State loaded")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than seeing
self.event_processor.add_event(MinerShieldEvent("State loaded"))
everywhere, I'd prefer to see
self._event("State loaded")
with
def _event(self, description: str, *args, **kwargs):
return self.event_processor.add_event(self.event_class(description, *args, **kwargs))
and event_class=MinerShieldEvent
in the constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed. But as for now I haven't added self.event_class, because it needs more changes - at least in AbstractMinerShieldEventProcessor, where type for event is hardcoded now. I think we can leave it for future.
bt_ddos_shield/miner_shield.py
Outdated
|
||
self.validators_manager.refresh_validators() | ||
validators: dict[Hotkey, str] = self.validators_manager.get_validators() | ||
self.event_processor.add_event(MinerShieldEvent(f"Validators refreshed, got {len(validators)} validators")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.event_processor.add_event(MinerShieldEvent(f"Validators refreshed, got {len(validators)} validators")) | |
self._event("Validators refreshed", validator_count=len(validators))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the MinerShieldEvent class will need to get a little bit more complicated, but we'll be able to emit smart objects that can then be filtered, sorted etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will change this in future commits, because this needs more work, but I added TODO in my notes. I also think I need to talk how this should done the best way, because there is PrintingMinerShieldEventProcessor for example.
bt_ddos_shield/miner_shield.py
Outdated
def __init__(self, task_name: str): | ||
""" | ||
Initialize task. | ||
|
||
Args: | ||
task_name: Short name of the task. | ||
""" | ||
self.task_name = task_name | ||
|
||
def handle(self, miner_shield: MinerShield): | ||
""" | ||
Run task logic. | ||
|
||
Args | ||
miner_shield: Instance of MinerShield in which task is executed. | ||
""" | ||
pass | ||
|
||
def __repr__(self): | ||
return self.task_name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def __init__(self, task_name: str): | |
""" | |
Initialize task. | |
Args: | |
task_name: Short name of the task. | |
""" | |
self.task_name = task_name | |
def handle(self, miner_shield: MinerShield): | |
""" | |
Run task logic. | |
Args | |
miner_shield: Instance of MinerShield in which task is executed. | |
""" | |
pass | |
def __repr__(self): | |
return self.task_name | |
NAME_DELETER = re.compile(r'^MinerShield(.*)Task$') | |
@abstractmethod | |
def run(self, miner_shield: MinerShield): | |
pass | |
def __repr__(self): | |
return self.NAME_DELETER.sub(r'\1', self.__class__.__name__) |
bt_ddos_shield/miner_shield.py
Outdated
pass | ||
|
||
|
||
class MinerShieldTask: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class MinerShieldTask: | |
class AbstractMinerShieldTask(ABC): |
bt_ddos_shield/miner_shield.py
Outdated
def handle(self, miner_shield: MinerShield): | ||
miner_shield._handle_initialize() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be refactored away into the abstract class too, but it's not worth it - we wouldn't save much anyway and it'd confuse static code analyzers
README.md
Outdated
|
||
<!-- | ||
@startuml ./assets/diagrams/CommunicationFlow | ||
participant Validator | ||
participant Miner | ||
participant GitHub | ||
participant AddressManager | ||
participant Storage | ||
participant Bittensor | ||
Validator -> Validator: Generate Validator key-pair | ||
Validator -> GitHub: Publish public key along with HotKey |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validator -> GitHub: Publish public key along with HotKey | |
Validator -> Bittensor: Publish public key along with HotKey |
README.md
Outdated
|
||
<!-- | ||
@startuml ./assets/diagrams/CommunicationFlow | ||
participant Validator | ||
participant Miner | ||
participant GitHub | ||
participant AddressManager | ||
participant Storage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
participant Storage | |
database Storage |
README.md
Outdated
|
||
<!-- | ||
@startuml ./assets/diagrams/CommunicationFlow | ||
participant Validator | ||
participant Miner | ||
participant GitHub | ||
participant AddressManager | ||
participant Storage | ||
participant Bittensor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
participant Bittensor | |
database Bittensor |
@@ -29,24 +29,27 @@ def __init__(self, address_id, address_type: AddressType, address: str, port: in | |||
self.address = address | |||
self.port = port | |||
|
|||
def __repr__(self): | |||
return f"Address(id={self.address_id}, type={self.address_type}, address={self.address}:{self.port})" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return f"Address(id={self.address_id}, type={self.address_type}, address={self.address}:{self.port})" | |
return f"Address(id={self.address_id}, address={self.address}:{self.port})" |
bt_ddos_shield/miner_shield.py
Outdated
|
||
while self.run: | ||
self.event_processor.add_event(MinerShieldEvent(f"Handling task {task}, try {try_count}")) | ||
self._event(f"Handling task {task}, try {try_count}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self._event("Handling task %{task}s, try %{try_count}s", task=task, try_count=try_count)
def _event(self, template, exception=None, **kwargs):
message = template.format(kwargs)
message_type = template
metadata = kwargs
# WHERE event.metadata ->> "try_count": > 1
# context can be pulled from stacktrace to get Classname.method_name
bt_ddos_shield/miner_shield.py
Outdated
""" | ||
Publish info about current manifest file to blockchain. | ||
""" | ||
current_address = Address.deserialize(self.blockchain_manager.get(self.miner_hotkey)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
current_address = Address.deserialize(self.blockchain_manager.get(self.miner_hotkey)) | |
current_address = self.ADDRESS_CLASS.deserialize(self.blockchain_manager.get(self.miner_hotkey)) |
class costam:
ADDRESS_CLASS: type = Address
class MyCostam:
ADDRESS_CLASS: type = MyAddress
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ClassVar[type[Address]]
though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've done it other, better way with usage of Serializer class
bt_ddos_shield/miner_shield.py
Outdated
""" | ||
address = self.manifest_manager.add_mapping_file(self.state_manager.get_state().active_addresses) | ||
self._event(f"Manifest updated, new address: {address}") | ||
self._add_task(MinerShieldPublishManifestTask(address)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...
|
||
def handle(self, miner_shield: MinerShield): | ||
class MinerShieldInitializeTask(AbstractMinerShieldTask): | ||
def run(self, miner_shield: MinerShield): | ||
miner_shield._handle_initialize() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hotkey: TypeAlias = str | ||
PublicKey: TypeAlias = str |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you develop at head and plan to use 3.12+, which I pretty much think is true, according to
Lines 8 to 9 in c8e6b96
[tool.poetry.dependencies] | |
python = "^3.12" |
...then I suggest you use type
statements from PEP 695 instead:
Hotkey: TypeAlias = str | |
PublicKey: TypeAlias = str | |
type Hotkey = str | |
type PublicKey = str |
Read more:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After this change, such constructs used in my code stopped working:
if isinstance(obj, Hotkey):
decoded_mapping[Hotkey(hotkey)]
So I'm not changing this, because I think TypeAlias is what I want here. Maybe in future HotKey will be some class and not just alias for type.
EDIT:
I've checked it and I can change these lines to:
if isinstance(obj, Hotkey.__value__):
decoded_mapping[Hotkey.__value__(hotkey)]
But it looks ugly IMHO. Maybe there is some better way?
tests/test_miner_shield.py
Outdated
@@ -14,8 +14,8 @@ def test_start_stop(self): | |||
""" | |||
Test if shield is properly starting and stopping. | |||
""" | |||
shield = MinerShield(None, None, None, None, | |||
None, LoggingMinerShieldEventProcessor(), MinerShieldOptions(retry_delay=1)) | |||
shield = MinerShield(None,None, None, None, None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shield = MinerShield(None,None, None, None, None, | |
shield = MinerShield(None, None, None, None, None, |
…m with not stable encryption)
…tion and handling shielded port change
|
||
self.task_queue.put(task) | ||
|
||
def _worker_function(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need threads? can't this be done with asyncio tasks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for example def _add_task(self, task: 'AbstractMinerShieldTask'):
could mostly be replaced with await task
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if it can be done with asyncio tasks, because I don't wait here for the result and task is retried forever by worker until shield gets stopped (this behaviour is by design). I will learn more about asyncio and then think if it can be changed, because my knowledge about asyncio is very small now. I wrote it in my notes for future upgrade but as for now I think this can be left as it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read something, thought about it, used AI and probably this can be refactored. But when current behavior is to be maintained, using asyncio won't change too much - I can use asyncio.to_thread to run _worker_function, use asyncio.Queue() instead of just Queue and make rest of needed changes, but logic and code structure will looks nearly the same (if I'm thinking correctly). So my question is, if it is worth time to change it, if it already works? I don't know the answer... Next time, when doing similar job, I'll try to use asyncio, but should I spend time for changing it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say this is worth refactoring - there's no point in reimplementing the logic of a lib so common as asyncio. (https://docs.python.org/3/library/asyncio-task.html#awaitables)
The refactor wouldn't take long and it'll make the code much easier to follow for any py dev.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll do it. If my thoughts about how to do it sounds good?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see note in https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread about the GIL - I'd rather use asyncio.create_task
instead of to_thread
since there's no need for multiple threads. For the "io heavy" db work there is optimized native asyncio support in sqlalchemy https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html.
the Queue is actually the event loop from asyncio - it shouldn't be needed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
although you might want to keep the queue just for preventing gc of the task... see Important:
https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
miner_shield._handle_initialize() | ||
|
||
|
||
class MinerShieldDisableTask(AbstractMinerShieldTask): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
insteaf of define these as classes - the functions could just be passed directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'm not sure about this. Adding these tasks as class allows us to print their names in logs when handling - I know I can use method.name for this but getting class name looks nicer for me. Also some tasks have params, so I will need to pass method and params to queue, so anyway I need to wrap them in some tuple or something - having class for this also looks nicer for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This goes together with the asyncio refactor suggested above - in a standard asyncio app you'd expect this to be passed as function (The tuple you mentioned translates to a closure)
Having a class for this looks nice from an OOP pov but it's unnatural in python.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll change it too
Merging PR after consultations with Even. More changes will be made. |
No description provided.