Skip to content

Commit

Permalink
feat(netobj): implemented zone based routing
Browse files Browse the repository at this point in the history
  • Loading branch information
Wizzerinus committed Dec 16, 2023
1 parent c2423e3 commit 556d2c1
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 18 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,10 @@ due to being opinionated are marked as domain-specific.
* **RAM persistence of fields** - implemented
* **Database persistence** - not implemented
* Most likely, I will implement three backends (SQL, MongoDB, dbm for local development).
* **Object API routing** - not implemented
* Zone-based message routing is planned (each client "sees" a set of zones,
and each object is in exactly one zone). The API already includes the
zone numbers on all objects, but they're currently not used.
* **Object API routing** - implemented
* Zone-based message routing is currently implemented (each client "sees" a set of zones,
and each object is in exactly one zone). The set of visible zones can be configured
through shared parameters (i.e., on handle-to-handle basis).

### Message and Connection Layer

Expand Down
62 changes: 62 additions & 0 deletions src/magicnet/batteries/middlewares/zone_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import dataclasses

from magicnet.core.connection import ConnectionHandle
from magicnet.core.net_globals import MNEvents, MNMathTargets
from magicnet.core.net_message import NetMessage
from magicnet.core.transport_handler import TransportMiddleware
from magicnet.netobjects.network_object import NetworkObject
from magicnet.protocol.protocol_globals import StandardMessageTypes
from magicnet.util.messenger import StandardEvents


@dataclasses.dataclass
class ZoneBasedRouter(TransportMiddleware):
# NOTE: the current implementation is probably quite slow
# (this is an euristic and I don't have any benchmarks, but it would
# have to run on a lower level than middlewares to speed it up)

PROCESSED_MESSAGE_TYPES = {
StandardMessageTypes.SET_OBJECT_FIELD,
StandardMessageTypes.GENERATE_OBJECT,
StandardMessageTypes.OBJECT_GENERATE_DONE,
StandardMessageTypes.DESTROY_OBJECT,
}

def __post_init__(self):
self.listen(MNEvents.BEFORE_LAUNCH, self.do_before_launch)

def do_before_launch(self):
self.add_message_operator(self.validate_message_zone, None)
self.add_math_target(MNMathTargets.VISIBLE_OBJECTS, self.only_visibles)

def only_visibles(self, objects: list[NetworkObject], handle: ConnectionHandle):
success, viszones = handle.get_shared_parameter("vz", list[int])
if not success:
self.emit(StandardEvents.WARNING, f"{handle.uuid}: incorrectly set viszones!")
return []

vz_set = set(viszones)
return [obj for obj in objects if obj.zone in vz_set]

def validate_message_zone(self, message: NetMessage, handle: ConnectionHandle):
if message.message_type not in self.PROCESSED_MESSAGE_TYPES:
return message

oid = message.parameters[0]
obj = self.transport.manager.managed_objects.get(oid)
if obj is None:
# Strange but ok
# Note that we still have to do this even if obj is falsey because
# we might be generating it still
self.emit(StandardEvents.WARNING, f"Message sent but the object is missing: {oid}")
return None

zone = obj.zone
success, viszones = handle.get_shared_parameter("vz", list[int])
if not success:
self.emit(StandardEvents.WARNING, f"{handle.uuid}: incorrectly set viszones!")
return None

if zone in viszones:
return message
return None
4 changes: 3 additions & 1 deletion src/magicnet/netobjects/network_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,9 @@ def call_field(

field.call(self, arguments)

def request_generate(self, owner: int = 0) -> None:
def request_generate(self, *, zone: int = None, owner: int = 0) -> None:
if zone is not None:
self.zone = zone
if self.manager.client_repository is not None:
# We have authority, we can create any objects
# or at least try to do it
Expand Down
6 changes: 3 additions & 3 deletions src/magicnet/netobjects/network_object_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ def send_network_object_generate(
msg.f_destination = handle
self.manager.send_message(msg)

def get_visible_objects(self, handle: ConnectionHandle):
def get_visible_objects(self, handle: ConnectionHandle) -> list[NetworkObject]:
return self.listener.calculate(
MNMathTargets.VISIBLE_OBJECTS, self.managed_objects, handle
MNMathTargets.VISIBLE_OBJECTS, list(self.managed_objects.values()), handle
)

def initialize_object(self, obj_id: int):
Expand Down Expand Up @@ -126,8 +126,8 @@ def create_object(self, obj: NetworkObject, owner: int = 0):

obj.oid = self.make_oid() + (self.manager.client_repository << 32)
obj.owner = owner or self.manager.client_repository
self.send_network_object_generate(obj, obj.get_loaded_params())
self.add_network_object(obj)
self.send_network_object_generate(obj, obj.get_loaded_params())
obj.object_state = ObjectState.GENERATING
self.initialize_object(obj.oid)

Expand Down
2 changes: 1 addition & 1 deletion src/magicnet/protocol/processors/network_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class MsgRequestVisible(MessageProcessor):

def invoke(self, message: NetMessage):
all_objects = self.manager.object_manager.get_visible_objects(message.sent_from)
for obj in all_objects.values():
for obj in all_objects:
params = obj.get_loaded_params()
self.manager.object_manager.send_network_object_generate(
obj, params, message.sent_from
Expand Down
60 changes: 60 additions & 0 deletions tests/net_objects/test_zones.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import dataclasses

from magicnet.batteries.middlewares.zone_routing import ZoneBasedRouter
from magicnet.netobjects.network_field import NetworkField
from magicnet.netobjects.network_object import NetworkObject
from net_objects.net_tester_netobj import FlexibleNetworkObjectTester


class ZoneTester(FlexibleNetworkObjectTester):
server_middlewares = [*FlexibleNetworkObjectTester.middlewares, ZoneBasedRouter]


def test_object_zones():
@dataclasses.dataclass
class TestNetObject(NetworkObject):
network_name = "test_obj"
object_role = 0

value: int = 0

@NetworkField
def set_value(self, value):
self.value = value

def net_create(self) -> None:
pass

def net_delete(self) -> None:
pass

tester = ZoneTester.create_and_start(TestNetObject, TestNetObject)

first_client = tester.make_client()
first_client.get_handle("server").set_shared_parameter("vz", [1])
second_client = tester.make_client()
second_client.get_handle("server").set_shared_parameter("vz", [2])

first_object = TestNetObject(tester.server)
first_object.send_message("set_value", [100])
first_object.request_generate(zone=1)

second_object = TestNetObject(tester.server)
second_object.send_message("set_value", [105])
second_object.request_generate(zone=2)

assert first_object.oid not in second_client.managed_objects
assert second_client.managed_objects.get(second_object.oid).value == 105
assert first_client.managed_objects.get(first_object.oid).value == 100
assert second_object.oid not in first_client.managed_objects

first_client.get_handle("server").set_shared_parameter("vz", [2])
second_client.get_handle("server").set_shared_parameter("vz", [1])
# Note: invisible object unloading is currently not implemented
first_client.object_manager.request_visible_objects()
assert first_client.managed_objects.get(second_object.oid).value == 105

second_object.send_message("set_value", [110])
assert first_client.managed_objects.get(second_object.oid).value == 110
# we get a discrepancy here because second client no longer sees updates
assert second_client.managed_objects.get(second_object.oid).value == 105
32 changes: 23 additions & 9 deletions tests/net_tester_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,28 @@ class FlexibleNetworkTester(NetworkTester, abc.ABC):
debug: bool = False

middlewares = [MessageValidatorMiddleware]
server_middlewares = [MessageValidatorMiddleware]
encoder = MsgpackEncoder()
transport = {
"client": {
"server": TransportParameters(
encoder, SingleAppTransport, None, middlewares
)

@classmethod
def transport(cls):
return {
"client": {
"server": TransportParameters(
cls.encoder, SingleAppTransport, None, cls.middlewares
)
}
}

@classmethod
def server_transport(cls):
return {
"client": {
"server": TransportParameters(
cls.encoder, SingleAppTransport, None, cls.server_middlewares
)
}
}
}

@classmethod
def create(cls, *args):
Expand All @@ -103,7 +117,7 @@ def raise_err(name, msg):

server = NetworkManager.create_root(
transport_type=EverywhereTransportManager,
transport_params=("server", cls.transport),
transport_params=("server", cls.server_transport()),
motd="An example native host",
client_repository=64,
)
Expand All @@ -117,13 +131,13 @@ def start(self):
def prepare_client(self, client: NetworkManager):
pass

def make_client(self):
def make_client(self) -> NetworkManager:
def raise_err(name, msg):
raise RuntimeError(f"{name} disconnected: {msg}")

client = NetworkManager.create_root(
transport_type=EverywhereTransportManager,
transport_params=("client", self.transport),
transport_params=("client", self.transport()),
)
client.listen(MNEvents.DISCONNECT, functools.partial(raise_err, "client"))
if self.debug:
Expand Down

0 comments on commit 556d2c1

Please sign in to comment.