Files
sublime-music/sublime/adapters/adapter_manager.py
2020-05-08 11:30:23 -06:00

286 lines
9.9 KiB
Python

import logging
from concurrent.futures import Future, ThreadPoolExecutor
from dataclasses import dataclass
from pathlib import Path
from typing import (
Any,
Callable,
Generic,
Optional,
Sequence,
Set,
Type,
TypeVar,
Union,
)
from sublime.config import AppConfiguration
from .adapter_base import Adapter, CacheMissError, CachingAdapter
from .api_objects import Playlist, PlaylistDetails
from .filesystem import FilesystemAdapter
from .subsonic import SubsonicAdapter
T = TypeVar("T")
class Result(Generic[T]):
"""
A result from a :class:`AdapterManager` function. This is effectively a wrapper
around a :class:`concurrent.futures.Future`, but it can also resolve immediately if
the data already exists.
"""
_data: Optional[T] = None
_future: Optional[Future] = None
on_cancel: Optional[Callable[[], None]] = None
def __init__(self, data_resolver: Union[T, Callable[[], T]]):
if callable(data_resolver):
def future_complete(f: Future):
self._data = f.result()
self._future = AdapterManager.executor.submit(data_resolver)
self._future.add_done_callback(future_complete)
else:
self._data = data_resolver
def result(self) -> T:
if self._data is not None:
return self._data
if self._future is not None:
return self._future.result()
raise Exception("AdapterManager.Result had neither _data nor _future member!")
def add_done_callback(self, fn: Callable, *args):
if self._future is not None:
self._future.add_done_callback(fn, *args)
else:
# Run the function immediately if it's not a future.
fn(self, *args)
def cancel(self) -> bool:
if self._future is not None:
return self._future.cancel()
return True
@property
def data_is_available(self) -> bool:
return self._data is not None
class AdapterManager:
available_adapters: Set[Any] = {FilesystemAdapter, SubsonicAdapter}
executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=50)
is_shutting_down: bool = False
@dataclass
class _AdapterManagerInternal:
ground_truth_adapter: Adapter
caching_adapter: Optional[CachingAdapter] = None
def shutdown(self):
self.ground_truth_adapter.shutdown()
if self.caching_adapter:
self.caching_adapter.shutdown()
_instance: Optional[_AdapterManagerInternal] = None
@staticmethod
def register_adapter(adapter_class: Type):
if not issubclass(adapter_class, Adapter):
raise TypeError("Attempting to register a class that is not an adapter.")
AdapterManager.available_adapters.add(adapter_class)
def __init__(self):
"""
This should not ever be called. You should only ever use the static methods on
this class.
"""
raise Exception(
"Do not instantiate the AdapterManager. "
"Only use the static methods on the class."
)
@staticmethod
def shutdown():
logging.info("AdapterManager shutdown start")
AdapterManager.is_shutting_down = True
AdapterManager.executor.shutdown()
if AdapterManager._instance:
AdapterManager._instance.shutdown()
logging.info("CacheManager shutdown complete")
@staticmethod
def reset(config: AppConfiguration):
# First, shutdown the current one...
if AdapterManager._instance:
AdapterManager._instance.shutdown()
# TODO: actually do stuff with the config to determine which adapters
# to create, etc.
assert config.server is not None
source_data_dir = Path(config.cache_location, config.server.strhash())
source_data_dir.joinpath("g").mkdir(parents=True, exist_ok=True)
source_data_dir.joinpath("c").mkdir(parents=True, exist_ok=True)
ground_truth_adapter_type = SubsonicAdapter
ground_truth_adapter = ground_truth_adapter_type(
{
key: getattr(config.server, key)
for key in ground_truth_adapter_type.get_config_parameters()
},
source_data_dir.joinpath("g"),
)
caching_adapter_type = FilesystemAdapter
caching_adapter = None
if caching_adapter_type and ground_truth_adapter_type.can_be_cached:
caching_adapter = caching_adapter_type(
{
key: getattr(config.server, key)
for key in caching_adapter_type.get_config_parameters()
},
source_data_dir.joinpath("c"),
is_cache=True,
)
AdapterManager._instance = AdapterManager._AdapterManagerInternal(
ground_truth_adapter, caching_adapter=caching_adapter,
)
@staticmethod
def can_get_playlists() -> bool:
# It only matters that the ground truth one can service the request.
return (
AdapterManager._instance is not None
and AdapterManager._instance.ground_truth_adapter.can_service_requests
and AdapterManager._instance.ground_truth_adapter.can_get_playlists
)
@staticmethod
def get_playlists(
before_download: Callable[[], None] = lambda: None,
force: bool = False, # TODO: rename to use_ground_truth_adapter?
) -> Result[Sequence[Playlist]]:
assert AdapterManager._instance
if (
not force
and AdapterManager._instance.caching_adapter
and AdapterManager._instance.caching_adapter.can_service_requests
and AdapterManager._instance.caching_adapter.can_get_playlists
):
try:
return Result(AdapterManager._instance.caching_adapter.get_playlists())
except CacheMissError:
logging.debug(f'Cache Miss on {"get_playlists"}.')
except Exception:
logging.exception(f'Error on {"get_playlists"} retrieving from cache.')
if (
AdapterManager._instance.ground_truth_adapter
and not AdapterManager._instance.ground_truth_adapter.can_service_requests
and not AdapterManager._instance.ground_truth_adapter.can_get_playlists
):
raise Exception(f'No adapters can service {"get_playlists"} at the moment.')
def future_fn() -> Sequence[Playlist]:
assert AdapterManager._instance
if before_download:
before_download()
return AdapterManager._instance.ground_truth_adapter.get_playlists()
future: Result[Sequence[Playlist]] = Result(future_fn)
if AdapterManager._instance.caching_adapter:
def future_finished(f: Future):
assert AdapterManager._instance
assert AdapterManager._instance.caching_adapter
AdapterManager._instance.caching_adapter.ingest_new_data(
CachingAdapter.FunctionNames.GET_PLAYLISTS, (), f.result(),
)
future.add_done_callback(future_finished)
return future
@staticmethod
def can_get_playlist_details() -> bool:
# It only matters that the ground truth one can service the request.
return (
AdapterManager._instance is not None
and AdapterManager._instance.ground_truth_adapter.can_service_requests
and AdapterManager._instance.ground_truth_adapter.can_get_playlist_details
)
@staticmethod
def get_playlist_details(
playlist_id: str,
before_download: Callable[[], None] = lambda: None,
force: bool = False, # TODO: rename to use_ground_truth_adapter?
) -> Result[PlaylistDetails]:
assert AdapterManager._instance
partial_playlist_data = None
if (
not force
and AdapterManager._instance.caching_adapter
and AdapterManager._instance.caching_adapter.can_service_requests
and AdapterManager._instance.caching_adapter.can_get_playlist_details
):
try:
return Result(
AdapterManager._instance.caching_adapter.get_playlist_details(
playlist_id
)
)
except CacheMissError as e:
partial_playlist_data = e.partial_data
logging.debug(f'Cache Miss on {"get_playlist_details"}.')
except Exception:
logging.exception(
f'Error on {"get_playlist_details"} retrieving from cache.'
)
if (
AdapterManager._instance.ground_truth_adapter
and not AdapterManager._instance.ground_truth_adapter.can_service_requests
and not (
AdapterManager._instance.ground_truth_adapter.can_get_playlist_details
)
):
if partial_playlist_data:
# TODO do something here
pass
raise Exception(
f'No adapters can service {"get_playlist_details"} at the moment.'
)
def future_fn() -> PlaylistDetails:
assert AdapterManager._instance
if before_download:
before_download()
return AdapterManager._instance.ground_truth_adapter.get_playlist_details(
playlist_id
)
future: Result[PlaylistDetails] = Result(future_fn)
if AdapterManager._instance.caching_adapter:
def future_finished(f: Future):
assert AdapterManager._instance
assert AdapterManager._instance.caching_adapter
AdapterManager._instance.caching_adapter.ingest_new_data(
CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS,
(playlist_id,),
f.result(),
)
future.add_done_callback(future_finished)
return future