Skip to content
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

Conversation

ggleszczynski
Copy link
Contributor

No description provided.

@ggleszczynski ggleszczynski force-pushed the features/implementation branch from 2275179 to 0dbf21f Compare November 2, 2024 11:14
Comment on lines 39 to 42
print(f"MinerShieldEvent: {event.event_description}\nException happened:")
print(traceback.format_exc())
else:
print(f"MinerShieldEvent: {event.event_description}")
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link

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.

Copy link
Contributor Author

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.


Args:
validator_hotkey: The hotkey of the validator.
"""
self._add_task(MinerShieldBanValidatorTask(validator_hotkey))
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pass

Comment on lines 110 to 115
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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).

Copy link
Contributor Author

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.

if not isinstance(task, MinerShieldTask):
raise Exception("Task is not instance of MinerShieldTask")
if not self.run:
raise Exception("Shield is disabled")
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines 130 to 132
try_count = 1

while self.run:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
try_count = 1
while self.run:
try_count: int = 0
while try_count <= self.options.max_retries:

Copy link
Contributor Author

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.

Initialize shield. Load state and initial validators set.
"""
self.state_manager.get_state()
self.event_processor.add_event(MinerShieldEvent("State loaded"))
Copy link
Contributor

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

Copy link
Contributor Author

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.


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"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.event_processor.add_event(MinerShieldEvent(f"Validators refreshed, got {len(validators)} validators"))
self._event("Validators refreshed", validator_count=len(validators)))

Copy link
Contributor

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

Copy link
Contributor Author

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.

Comment on lines 222 to 241
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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__)

pass


class MinerShieldTask:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class MinerShieldTask:
class AbstractMinerShieldTask(ABC):

Comment on lines 247 to 248
def handle(self, miner_shield: MinerShield):
miner_shield._handle_initialize()
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
participant Storage
database Storage

README.md Outdated

<!--
@startuml ./assets/diagrams/CommunicationFlow
participant Validator
participant Miner
participant GitHub
participant AddressManager
participant Storage
participant Bittensor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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})"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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})"


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}")
Copy link
Contributor

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

"""
Publish info about current manifest file to blockchain.
"""
current_address = Address.deserialize(self.blockchain_manager.get(self.miner_hotkey))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassVar[type[Address]] though?

Copy link
Contributor Author

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

"""
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))
Copy link
Contributor

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()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Comment on lines +3 to +4
Hotkey: TypeAlias = str
PublicKey: TypeAlias = str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Copy link

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

[tool.poetry.dependencies]
python = "^3.12"

...then I suggest you use type statements from PEP 695 instead:

Suggested change
Hotkey: TypeAlias = str
PublicKey: TypeAlias = str
type Hotkey = str
type PublicKey = str

Read more:

Copy link
Contributor Author

@ggleszczynski ggleszczynski Dec 6, 2024

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:

  1. if isinstance(obj, Hotkey):
  2. 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:

  1. if isinstance(obj, Hotkey.__value__):
  2. decoded_mapping[Hotkey.__value__(hotkey)]

But it looks ugly IMHO. Maybe there is some better way?

@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
shield = MinerShield(None,None, None, None, None,
shield = MinerShield(None, None, None, None, None,

@bswck
Copy link

bswck commented Nov 28, 2024

See also #9 and #10. GLHF working on this!

@ggleszczynski
Copy link
Contributor Author

See also #9 and #10. GLHF working on this!

Checked it. And thanks :-)

bt_ddos_shield/manifest_manager.py Show resolved Hide resolved
bt_ddos_shield/manifest_manager.py Show resolved Hide resolved
bt_ddos_shield/manifest_manager.py Outdated Show resolved Hide resolved
bt_ddos_shield/manifest_manager.py Show resolved Hide resolved

self.task_queue.put(task)

def _worker_function(self):

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?

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

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.

Copy link
Contributor Author

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?

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

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):

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

Copy link
Contributor Author

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.

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.

Copy link
Contributor Author

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

bt_ddos_shield/state_manager.py Show resolved Hide resolved
bt_ddos_shield/state_manager.py Show resolved Hide resolved
@zyzniewski-reef
Copy link

@grzegorz-leszczynski-reef
Copy link
Contributor

Merging PR after consultations with Even. More changes will be made.

@grzegorz-leszczynski-reef grzegorz-leszczynski-reef merged commit a50bff9 into bactensor:master Jan 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants