Skip to content

Commit

Permalink
Agent: Auto Return Reservations
Browse files Browse the repository at this point in the history
It's easy to forget returning the reservation. Add auto return timer, so
that places are automatically returned after a configurable delay. The
default delay is 10 hours.
  • Loading branch information
holesch committed Aug 16, 2024
1 parent 7cb218d commit 5450cd8
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 3 deletions.
31 changes: 31 additions & 0 deletions doc/reference/import-description.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,37 @@ The *Agent* uses this import description to reserve one of the matching

## Settings

### `auto_return_time`

**Type:** String \
**Required:** No \
**Default:** `"10h"`

Delay after which the reservation is automatically returned. The timer is reset
after editing the import description with the `not-my-board edit` command.

The time format uses a sequence of positive integers followed by lowercase time
units:

:::{table}
:align: left

Unit | Description
---- | -----------
`w` | weeks
`d` | days
`h` | hours
`m` | minutes
`s` | seconds (optional)
:::

Units must appear in descending order of significance (e.g. weeks before days).

Examples:
- `600`: 600 seconds or 10 minutes
- `10m`: 10 minutes
- `1h30m`: 1 hour 30 minutes (90 minutes)

### `parts`

**Type:** Table \
Expand Down
41 changes: 38 additions & 3 deletions not_my_board/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import urllib.parse
import weakref
from dataclasses import dataclass, field
from typing import Mapping, Tuple
from typing import Mapping, Optional, Tuple

import not_my_board._jsonrpc as jsonrpc
import not_my_board._models as models
Expand Down Expand Up @@ -135,15 +135,21 @@ async def serve_forever(self):
await self._unix_server.serve_forever()

async def _cleanup(self):
# The auto return tasks might currently close tunnels. Cancel auto
# return tasks first.
await util.cancel_tasks(
[r.auto_return_task for r in self._reservations.values()]
)

coros = [
t.close() for r in self._reservations.values() for t in r.tunnels.values()
]

await util.run_concurrently(*coros)

async def reserve(self, name, import_description_toml):
parsed = util.toml_loads(import_description_toml)
import_description = models.ImportDesc(name=name, **parsed)
auto_return_time = util.parse_time(import_description.auto_return_time)

async with self._name_lock(name):
if name in self._reservations:
Expand All @@ -169,8 +175,12 @@ async def reserve(self, name, import_description_toml):
desc: desc.tunnel_cls(desc, self._hub_host, self._io)
for desc in tunnel_descs_by_id[place_id]
}

coro = self._auto_return(name, auto_return_time)
auto_return_task = asyncio.create_task(coro)

self._reservations[name] = _Reservation(
import_description_toml, place, tunnels
import_description_toml, place, tunnels, auto_return_task
)

async def return_reservation(self, name, force=False):
Expand All @@ -181,6 +191,7 @@ async def return_reservation(self, name, force=False):
else:
raise RuntimeError(f'Place "{name}" is still attached')
await self._hub.return_reservation(reservation.place.id)
await util.cancel_tasks([reservation.auto_return_task])
del self._reservations[name]

async def attach(self, name):
Expand All @@ -206,6 +217,23 @@ async def _detach_reservation(self, reservation):
await util.run_concurrently(*coros)
reservation.is_attached = False

async def _auto_return(self, name, timeout): # noqa: ASYNC109
try:
if timeout == 0:
# disable auto return, wait forever
await asyncio.Event().wait()

await asyncio.sleep(timeout)
logger.info('Auto return timeout: Returning place "%s"', name)

async with self._reservation(name) as reservation:
if reservation.is_attached:
await self._detach_reservation(reservation)
await self._hub.return_reservation(reservation.place.id)
del self._reservations[name]
except Exception as e:
logger.warning("Auto return failed: %s", e)

@contextlib.asynccontextmanager
async def _name_lock(self, name):
if name not in self._locks:
Expand Down Expand Up @@ -253,6 +281,7 @@ async def update_import_description(self, name, import_description_toml):
async with self._reservation(name) as reservation:
parsed = util.toml_loads(import_description_toml)
import_description = models.ImportDesc(name=name, **parsed)
auto_return_time = util.parse_time(import_description.auto_return_time)

imported_part_sets = [
(name, _part_to_set(imported_part))
Expand Down Expand Up @@ -298,6 +327,11 @@ async def restore_removed():
]
await util.run_concurrently(*coros)

# refresh auto return
await util.cancel_tasks([reservation.auto_return_task])
coro = self._auto_return(name, auto_return_time)
reservation.auto_return_task = asyncio.create_task(coro)

# everything ok: update reservation
reservation.import_description_toml = import_description_toml
reservation.tunnels = new_tunnels
Expand Down Expand Up @@ -496,6 +530,7 @@ class _Reservation:
place: models.Place
is_attached: bool = field(default=False, init=False)
tunnels: Mapping[_TunnelDesc, _Tunnel]
auto_return_task: Optional[asyncio.Task]


class ProtocolError(Exception):
Expand Down

0 comments on commit 5450cd8

Please sign in to comment.