Skip to content

Commit a372745

Browse files
RSDK-5109 - add close function (#458)
1 parent d256ed8 commit a372745

File tree

17 files changed

+154
-15
lines changed

17 files changed

+154
-15
lines changed

docs/examples/example.ipynb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@
269269
"from typing import Any, Dict, Mapping, Optional\n",
270270
"\n",
271271
"from viam.components.sensor import Sensor\n",
272+
"from viam.logging import getLogger\n",
273+
"\n",
274+
"LOGGER = getLogger(__name__)\n",
272275
"\n",
273276
"\n",
274277
"class MySensor(Sensor):\n",
@@ -282,6 +285,11 @@
282285
" async def get_geometries(self, *, extra: Optional[Dict[str, Any]] = None, timeout: Optional[float] = None) -> List[Geometry]:\n",
283286
" raise NotImplementedError\n",
284287
"\n",
288+
" def close(self):\n",
289+
" # This is a completely optional function to include. This will be called when the resource is removed from the config or the module\n",
290+
" # is shutting down.\n",
291+
" LOGGER.debug(f\"{self.name} is closed.\")\n",
292+
"\n",
285293
"# Anything below this line is optional and will be replaced later, but may come in handy for debugging and testing.\n",
286294
"# To use, call `python wifi_sensor_module.py` in the command line while in the `src` directory.\n",
287295
"async def main():\n",

docs/examples/module_step2.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
from typing_extensions import Self
55

66
from viam.components.sensor import Sensor
7+
from viam.logging import getLogger
78
from viam.proto.app.robot import ComponentConfig
89
from viam.proto.common import ResourceName
910
from viam.resource.base import ResourceBase
1011
from viam.resource.registry import Registry, ResourceCreatorRegistration
1112
from viam.resource.types import Model, ModelFamily
1213

14+
LOGGER = getLogger(__name__)
15+
1316

1417
class MySensor(Sensor):
1518
# Subclass the Viam Sensor component and implement the required functions
@@ -26,6 +29,11 @@ async def get_readings(self, extra: Optional[Dict[str, Any]] = None, **kwargs) -
2629
wifi_signal = [x for x in content[2].split(" ") if x != ""]
2730
return {"link": wifi_signal[2], "level": wifi_signal[3], "noise": wifi_signal[4]}
2831

32+
def close(self):
33+
# This is a completely optional function to include. This will be called when the resource is removed from the config or the module
34+
# is shutting down.
35+
LOGGER.debug(f"{self.name} is closed.")
36+
2937

3038
async def main():
3139
Registry.register_resource_creator(Sensor.SUBTYPE, MySensor.MODEL, ResourceCreatorRegistration(MySensor.new))

docs/examples/module_step2_optional.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
from typing_extensions import Self
55

66
from viam.components.sensor import Sensor
7+
from viam.logging import getLogger
78
from viam.proto.app.robot import ComponentConfig
89
from viam.proto.common import ResourceName
910
from viam.resource.base import ResourceBase
1011
from viam.resource.registry import Registry, ResourceCreatorRegistration
1112
from viam.resource.types import Model, ModelFamily
1213

14+
LOGGER = getLogger(__name__)
15+
1316

1417
class MySensor(Sensor):
1518
# Subclass the Viam Sensor component and implement the required functions
@@ -48,6 +51,11 @@ def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceNam
4851
multiplier = 1.0
4952
self.multiplier = multiplier
5053

54+
def close(self):
55+
# This is a completely optional function to include. This will be called when the resource is removed from the config or the module
56+
# is shutting down.
57+
LOGGER.debug(f"{self.name} is closed.")
58+
5159

5260
async def main():
5361
Registry.register_resource_creator(Sensor.SUBTYPE, MySensor.MODEL, ResourceCreatorRegistration(MySensor.new, MySensor.validate_config))

docs/examples/module_step3.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
from typing_extensions import Self
55

66
from viam.components.sensor import Sensor
7+
from viam.logging import getLogger
78
from viam.module.module import Module
89
from viam.proto.app.robot import ComponentConfig
910
from viam.proto.common import ResourceName
1011
from viam.resource.base import ResourceBase
1112
from viam.resource.registry import Registry, ResourceCreatorRegistration
1213
from viam.resource.types import Model, ModelFamily
1314

15+
LOGGER = getLogger(__name__)
16+
1417

1518
class MySensor(Sensor):
1619
# Subclass the Viam Sensor component and implement the required functions
@@ -27,6 +30,11 @@ async def get_readings(self, extra: Optional[Dict[str, Any]] = None, **kwargs) -
2730
wifi_signal = [x for x in content[2].split(" ") if x != ""]
2831
return {"link": wifi_signal[2], "level": wifi_signal[3], "noise": wifi_signal[4]}
2932

33+
def close(self):
34+
# This is a completely optional function to include. This will be called when the resource is removed from the config or the module
35+
# is shutting down.
36+
LOGGER.debug(f"{self.name} is closed.")
37+
3038

3139
async def main():
3240
"""

docs/examples/my_cool_arm.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
from typing import Any, Dict, List, Optional, Tuple
66

77
from viam.components.arm import Arm, JointPositions, KinematicsFileFormat, Pose
8+
from viam.logging import getLogger
89
from viam.operations import run_with_operation
910
from viam.proto.common import Capsule, Geometry, Sphere
1011

12+
LOGGER = getLogger(__name__)
13+
1114

1215
class MyCoolArm(Arm):
1316
# Subclass the Viam Arm component and implement the required functions
@@ -102,3 +105,8 @@ async def get_geometries(self, *, extra: Optional[Dict[str, Any]] = None, timeou
102105

103106
async def get_kinematics(self, extra: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[KinematicsFileFormat.ValueType, bytes]:
104107
return KinematicsFileFormat.KINEMATICS_FILE_FORMAT_SVA, self.kinematics
108+
109+
def close(self):
110+
# This is a completely optional function to include. This will be called when the resource is removed from the config or the module
111+
# is shutting down.
112+
LOGGER.debug(f"{self.name} is closed.")

examples/complex_module/src/arm/my_arm.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
from typing_extensions import Self
66

77
from viam.components.arm import Arm, JointPositions, KinematicsFileFormat, Pose
8+
from viam.logging import getLogger
89
from viam.operations import run_with_operation
910
from viam.proto.app.robot import ComponentConfig
1011
from viam.proto.common import Capsule, Geometry, ResourceName, Sphere
1112
from viam.resource.base import ResourceBase
1213
from viam.resource.types import Model, ModelFamily
1314

15+
LOGGER = getLogger(__name__)
16+
1417

1518
class MyArm(Arm):
1619
# Subclass the Viam Arm component and implement the required functions
@@ -104,3 +107,8 @@ async def get_kinematics(self, extra: Optional[Dict[str, Any]] = None, **kwargs)
104107
with open(filepath, mode="rb") as f:
105108
file_data = f.read()
106109
return (KinematicsFileFormat.KINEMATICS_FILE_FORMAT_SVA, file_data)
110+
111+
def close(self):
112+
# This is a completely optional function to include. This will be called when the resource is removed from the config or the module
113+
# is shutting down.
114+
LOGGER.debug(f"{self.name} is closed.")

examples/complex_module/src/gizmo/my_gizmo.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing_extensions import Self
44

5+
from viam.logging import getLogger
56
from viam.module.types import Reconfigurable
67
from viam.proto.app.robot import ComponentConfig
78
from viam.proto.common import ResourceName
@@ -10,6 +11,8 @@
1011

1112
from ..gizmo.api import Gizmo
1213

14+
LOGGER = getLogger(__name__)
15+
1316

1417
class MyGizmo(Gizmo, Reconfigurable):
1518
"""This is the specific implementation of a ``Gizmo`` (defined in api.py).
@@ -68,3 +71,8 @@ async def do_two(self, arg1: bool, **kwargs) -> str:
6871

6972
def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]):
7073
self.my_arg = config.attributes.fields["arg1"].string_value
74+
75+
def close(self):
76+
# This is a completely optional function to include. This will be called when the resource is removed from the config or the module
77+
# is shutting down.
78+
LOGGER.debug(f"{self.name} is closed.")

examples/simple_module/src/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing_extensions import Self
55

66
from viam.components.sensor import Sensor
7+
from viam.logging import getLogger
78
from viam.module.module import Module
89
from viam.proto.app.robot import ComponentConfig
910
from viam.proto.common import ResourceName
@@ -12,6 +13,8 @@
1213
from viam.resource.types import Model, ModelFamily
1314
from viam.utils import ValueTypes
1415

16+
LOGGER = getLogger(__name__)
17+
1518

1619
class MySensor(Sensor):
1720
# Subclass the Viam Sensor component and implement the required functions
@@ -49,6 +52,11 @@ def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceNam
4952
multiplier = 1.0
5053
self.multiplier = multiplier
5154

55+
def close(self):
56+
# This is a completely optional function to include. This will be called when the resource is removed from the config or the module
57+
# is shutting down.
58+
LOGGER.debug(f"{self.name} is closed.")
59+
5260

5361
async def main():
5462
"""This function creates and starts a new module, after adding all desired resource models.

src/viam/module/module.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ async def reconfigure_resource(self, request: ReconfigureResourceRequest):
150150
else:
151151
resource.stop()
152152
add_request = AddResourceRequest(config=request.config, dependencies=request.dependencies)
153-
self.server.remove_resource(rn)
153+
await self.server.remove_resource(rn)
154154
await self.add_resource(add_request)
155155

156156
async def remove_resource(self, request: RemoveResourceRequest):
@@ -161,7 +161,7 @@ async def remove_resource(self, request: RemoveResourceRequest):
161161
await resource.stop()
162162
else:
163163
resource.stop()
164-
self.server.remove_resource(rn)
164+
await self.server.remove_resource(rn)
165165

166166
async def ready(self, request: ReadyRequest) -> ReadyResponse:
167167
self._parent_address = request.parent_address

src/viam/resource/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,12 @@ def get_operation(self, kwargs: Mapping[str, Any]) -> Operation:
8484
viam.operations.Operation: The operation associated with this function
8585
"""
8686
return kwargs.get(Operation.ARG_NAME, Operation._noop())
87+
88+
async def close(self):
89+
"""Safely shut down the resource and prevent further use.
90+
91+
Close must be idempotent. Later configuration may allow a resource to be "open" again.
92+
If a resource does not want or need a close function, it is assumed that the resource does not need to retun errors when future
93+
non-Close methods are called.
94+
"""
95+
return

src/viam/resource/manager.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from threading import RLock
22
from typing import Dict, List, Type, TypeVar
33

4+
from viam.logging import getLogger
45
from viam.proto.common import ResourceName
56
from viam.resource.base import ResourceBase
67
from viam.resource.registry import Registry
@@ -9,6 +10,7 @@
910
from ..errors import DuplicateResourceError, ResourceNotFoundError
1011
from ..services.service_base import ServiceBase
1112

13+
LOGGER = getLogger(__name__)
1214
ResourceType = TypeVar("ResourceType", bound=ResourceBase)
1315

1416

@@ -91,14 +93,31 @@ def get_resource(self, of_type: Type[ResourceType], name: ResourceName) -> Resou
9193
return self.get_resource(of_type, self._short_to_long_name[name.name][0])
9294
raise ResourceNotFoundError(name.subtype, name.name)
9395

94-
def remove_resource(self, name: ResourceName):
96+
async def remove_resource(self, name: ResourceName):
9597
"""Remove the resource with the specified ```ResourceName```.
9698
9799
Args:
98100
name (viam.proto.common.ResourceName): The ResourceName of the resource
99101
"""
100102
with self._lock:
101-
del self.resources[name]
103+
try:
104+
resource = self.resources[name]
105+
await resource.close()
106+
except Exception as e:
107+
raise e
108+
finally:
109+
del self.resources[name]
110+
111+
async def close(self):
112+
"""Close the resourcce manager by removing all resources.
113+
Please note that any errors will not raise an exception. Errors will still be logged."""
114+
rns = [key for key in self.resources.keys()]
115+
with self._lock:
116+
for rn in rns:
117+
try:
118+
await self.remove_resource(rn)
119+
except Exception as e:
120+
LOGGER.error(f"Error while closing {rn.name}:", e)
102121

103122
def _resource_by_name_only(self, name: str) -> ResourceBase:
104123
for rname, resource in self.resources.items():

src/viam/robot/client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,15 @@ async def refresh(self):
213213
if rname.subtype == Sensor.SUBTYPE.resource_subtype and MovementSensor.get_resource_name(rname.name) in resource_names:
214214
continue
215215

216-
self._create_or_reset_client(rname)
216+
await self._create_or_reset_client(rname)
217217

218218
for rname in self.resource_names:
219219
if rname not in resource_names:
220-
self._manager.remove_resource(rname)
220+
await self._manager.remove_resource(rname)
221221

222222
self._resource_names = resource_names
223223

224-
def _create_or_reset_client(self, resourceName: ResourceName):
224+
async def _create_or_reset_client(self, resourceName: ResourceName):
225225
if resourceName in self._manager.resources:
226226
res = self._manager.get_resource(ResourceBase, resourceName)
227227

@@ -233,7 +233,7 @@ def _create_or_reset_client(self, resourceName: ResourceName):
233233
if isinstance(res, ReconfigurableResourceRPCClientBase):
234234
res.reset_channel(self._channel)
235235
else:
236-
self._manager.remove_resource(resourceName)
236+
await self._manager.remove_resource(resourceName)
237237
self._manager.register(
238238
Registry.lookup_subtype(Subtype.from_resource_name(resourceName)).create_rpc_client(resourceName.name, self._channel)
239239
)

src/viam/rpc/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ async def serve(
113113
await self._server.start(host, port)
114114
LOGGER.info(f"Serving on {host}:{port}")
115115
await self._server.wait_closed()
116+
await self.close()
116117
LOGGER.debug("gRPC server closed")
117118

118119
@classmethod

tests/mocks/module/gizmo/my_gizmo.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
class MyGizmo(Gizmo, Reconfigurable):
1616
MODEL: ClassVar[Model] = Model(ModelFamily("acme", "demo"), "mygizmo")
1717
my_arg: str
18+
closed: bool = False
1819

1920
@classmethod
2021
def new(cls, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]) -> Self:
@@ -59,3 +60,7 @@ async def do_two(self, arg1: bool, **kwargs) -> str:
5960

6061
def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceName, ComponentBase]):
6162
self.my_arg = config.attributes.fields["arg1"].string_value
63+
64+
async def close(self):
65+
self.closed = True
66+
return

tests/test_module.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,23 @@ async def test_add_resource_with_deps(self, robot_service: RobotService): # noq
146146

147147
@pytest.mark.asyncio
148148
async def test_remove_resource(self):
149+
gizmo = self.module.server.get_resource(MyGizmo, Gizmo.get_resource_name("gizmo1"))
150+
assert gizmo.closed is False
149151
assert Gizmo.get_resource_name("gizmo1") in self.module.server.resources
150152
req = RemoveResourceRequest(name="acme:component:gizmo/gizmo1")
151153
await self.module.remove_resource(req)
152154
assert Gizmo.get_resource_name("gizmo1") not in self.module.server.resources
155+
assert gizmo.closed is True
153156

154-
assert SummationService.get_resource_name("mysum1") in self.module.server.resources
155-
req = RemoveResourceRequest(name="acme:service:summation/mysum1")
156-
await self.module.remove_resource(req)
157-
assert SummationService.get_resource_name("mysum1") not in self.module.server.resources
157+
with mock.patch("tests.mocks.module.summation.MySummationService.close") as mocked:
158+
assert SummationService.get_resource_name("mysum1") in self.module.server.resources
159+
req = RemoveResourceRequest(name="acme:service:summation/mysum1")
160+
161+
mocked.assert_not_called()
162+
await self.module.remove_resource(req)
163+
assert SummationService.get_resource_name("mysum1") not in self.module.server.resources
164+
# test default close
165+
mocked.assert_called_once()
158166

159167
@pytest.mark.asyncio
160168
async def test_ready(self):

0 commit comments

Comments
 (0)