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

Implementation of StubDataClayObject #55

Merged
merged 15 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dataclay-common
34 changes: 34 additions & 0 deletions examples/stub/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
services:

redis:
image: redis:latest
ports:
- 6379:6379

metadata-service:
image: "ghcr.io/bsc-dom/dataclay:dev"
depends_on:
- redis
ports:
- 16587:16587
environment:
- DATACLAY_KV_HOST=redis
- DATACLAY_PASSWORD=s3cret
- DATACLAY_USERNAME=testuser
- DATACLAY_DATASET=testdata
- DATACLAY_LOGLEVEL=debug
command: python -m dataclay.metadata
volumes:
- ../..:/app:ro

backend:
image: "ghcr.io/bsc-dom/dataclay:dev"
depends_on:
- redis
environment:
- DATACLAY_KV_HOST=redis
- DATACLAY_LOGLEVEL=debug
command: python -m dataclay.backend
volumes:
- ./model:/workdir/model:ro
- ../..:/app:ro
55 changes: 55 additions & 0 deletions examples/stub/isolated_client/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from dataclay import Client, StubDataClayObject

client = Client(host="127.0.0.1")
client.start()

# Instantiate the stubs of remote classes
PersonStub = StubDataClayObject["model.family.Person"]
DogStub = StubDataClayObject["model.family.Dog"]

# Create a Person and a Dog from the stubs
person = PersonStub(name="Alice", age=30)
dog = DogStub(name="Rex", age=5)

# Check the person and dog are created correctly
assert person._dc_is_registered is True
assert person.name == "Alice"
assert person.age == 30
assert dog._dc_is_registered is True
assert dog.name == "Rex"
assert dog.age == 5

# Check get and set attribute of the person
assert person.name == "Alice"
assert person.age == 30
person.age = 31
assert person.age == 31

# Check calling activemethod of the person
person.add_year()
assert person.age == 32

# Set the dog to the person
person.dog = dog

# Check the dog is set correctly
assert person.dog.name == "Rex"
assert person.dog.age == 5

# Add alias to the person
person.add_alias("person_alias")

# Get the person by alias
person_by_alias = PersonStub.get_by_alias("person_alias")

# Check the person is the same as the original
assert person_by_alias == person

# Set a puppy to the dog
puppy = dog.new_puppy("Bobby")

# Check the puppy is created correctly
assert puppy.name == "Bobby"

# Check the puppy is added to the dog
assert puppy in dog.puppies
126 changes: 126 additions & 0 deletions examples/stub/model/family.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from __future__ import annotations

from dataclay import DataClayObject, activemethod
from dataclay.event_loop import run_dc_coroutine


class Person(DataClayObject):
name: str
age: int
spouse: Person
dog: Dog

@activemethod
def __init__(self, name, age):
self.name = name
self.age = age
self.spouse = None
self.dog = None

@activemethod
def add_year(self):
self.age += 1


class Dog(DataClayObject):
name: str
age: int
puppies: list[Dog]

@activemethod
def __init__(self, name, age):
self.name = name
self.age = age
self.dog_age = age * 7
self.puppies = []

@activemethod
def add_year(self):
self.age += 1
self.dog_age = self.age * 7

@activemethod
def get_dog_age(self):
try:
return self.dog_age
except Exception:
self.dog_age = self.age * 7
return self.dog_age

@activemethod
def new_puppy(self, name):
puppy = Dog(name, 0)
self.puppies.append(puppy)
return puppy


class Family(DataClayObject):
members: list[Person | Dog]

@activemethod
def __init__(self, *args):
self.members = list(args)

@activemethod
def add(self, new_member: Person | Dog):
self.members.append(new_member)

@activemethod
def __str__(self) -> str:
result = ["Members:"]

for p in self.members:
result.append(" - Name: %s, age: %d" % (p.name, p.age))

return "\n".join(result)

@activemethod
def add_year(self):
for p in self.members:
p.add_year()

# WARNING: Logic changed. Flush all now unloads all objects,
# enven those that are running activemethods. This test is not valid anymore,
# and will fail. Mutable attributes are not guaranteed to be consistent. To
# guarantee consistency, use immutable attributes. The below code would work
# if members was reassigned to the self.members attribute.
@activemethod
def test_self_is_not_unloaded(self):
"""Testing that while executing the activemethod in a Backend,
the current instance must not be unloaded/nullified, in order to
guarantee properties mutability.
"""
from dataclay.config import get_runtime

members = self.members

run_dc_coroutine(get_runtime().data_manager.flush_all)

dog = Dog("Rio", 4)
members.append(dog)

##################
# NOTE: Corrected line (check WARNING above)
self.members = members
##################

# If the current instance was nullified,
# mutable "members" won't be consistent with the attribute
assert members is self.members

@activemethod
def test_reference_is_unloaded(self):
"""
DataClayObjects that are not self, can be unloaded in memory pressure.
"""
from dataclay.config import get_runtime

new_family = Family()
members = new_family.members

run_dc_coroutine(get_runtime().data_manager.flush_all)

dog = Dog("Rio", 4)
members.append(dog)
assert members is not new_family.members
assert members != new_family.members
10 changes: 9 additions & 1 deletion src/dataclay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@

from dataclay.client.api import Client
from dataclay.dataclay_object import DataClayObject, activemethod
from dataclay.stub import StubDataClayObject

from dataclay.alien import AlienDataClayObject # isort: skip

StorageObject = DataClayObject

__version__ = "4.2.0.dev"
__all__ = ["Client", "DataClayObject", "AlienDataClayObject", "activemethod", "StorageObject"]
__all__ = [
"Client",
"DataClayObject",
"AlienDataClayObject",
"StubDataClayObject",
"activemethod",
"StorageObject",
]
6 changes: 6 additions & 0 deletions src/dataclay/backend/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,9 @@ async def stop(self):
@grpc_aio_error_handler
async def drain(self):
await self.stub.Drain(empty_pb2.Empty())

@grpc_aio_error_handler
async def get_class_info(self, class_name):
request = backend_pb2.GetClassInfoRequest(class_name=class_name)
response = await self.stub.GetClassInfo(request, metadata=self.metadata_call)
return response.properties, response.activemethods
7 changes: 7 additions & 0 deletions src/dataclay/backend/servicer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)
from grpc_health.v1 import health, health_pb2, health_pb2_grpc

from dataclay import utils
from dataclay.backend.api import BackendAPI
from dataclay.config import session_var, settings
from dataclay.event_loop import get_dc_event_loop, set_dc_event_loop
Expand Down Expand Up @@ -339,3 +340,9 @@ async def NewObjectReplica(self, request, context):
request.remotes,
)
return Empty()

@ServicerMethod(backend_pb2.GetClassInfoResponse)
async def GetClassInfo(self, request, context):
cls = utils.get_class_by_name(request.class_name)
properties, activemethods = utils.get_class_info(cls)
return backend_pb2.GetClassInfoResponse(properties=properties, activemethods=activemethods)
24 changes: 19 additions & 5 deletions src/dataclay/proto/backend/backend_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading