Use a deterministic hash

This commit is contained in:
Sumner Evans
2020-04-23 10:44:55 -06:00
parent a36aac26e9
commit d28c389bb2
5 changed files with 87 additions and 32 deletions

View File

@@ -1,4 +1,7 @@
import hashlib
import json
import logging import logging
from base64 import b64encode
from dataclasses import asdict from dataclasses import asdict
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -54,6 +57,11 @@ class FilesystemAdapter(CachingAdapter):
# ========================================================================= # =========================================================================
can_service_requests: bool = True can_service_requests: bool = True
# Data Helper Methods
# =========================================================================
def _params_hash(self, *params: Any) -> str:
return hashlib.sha1(bytes(json.dumps(params), 'utf8')).hexdigest()
# Data Retrieval Methods # Data Retrieval Methods
# ========================================================================= # =========================================================================
can_get_playlists: bool = True can_get_playlists: bool = True
@@ -87,7 +95,8 @@ class FilesystemAdapter(CachingAdapter):
function_name = CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS function_name = CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS
cache_info = models.CacheInfo.get_or_none( cache_info = models.CacheInfo.get_or_none(
models.CacheInfo.query_name == function_name, models.CacheInfo.query_name == function_name,
params_hash=hash((playlist_id, ), )) params_hash=self._params_hash(playlist_id),
)
if not cache_info: if not cache_info:
raise CacheMissError(partial_data=playlist) raise CacheMissError(partial_data=playlist)
@@ -106,7 +115,7 @@ class FilesystemAdapter(CachingAdapter):
models.CacheInfo.insert( models.CacheInfo.insert(
query_name=function, query_name=function,
params_hash=hash(params), params_hash=self._params_hash(*params),
last_ingestion_time=datetime.now(), last_ingestion_time=datetime.now(),
).on_conflict_replace().execute() ).on_conflict_replace().execute()

View File

@@ -3,6 +3,7 @@ from typing import Any, Optional, Sequence
from peewee import ( from peewee import (
BooleanField, BooleanField,
CompositeKey,
DoubleField, DoubleField,
ensure_tuple, ensure_tuple,
ForeignKeyField, ForeignKeyField,
@@ -23,14 +24,6 @@ database = SqliteDatabase(None)
# Custom Fields # Custom Fields
# ============================================================================= # =============================================================================
class DurationField(DoubleField):
def db_value(self, value: timedelta) -> Optional[float]:
return value.total_seconds() if value else None
def python_value(self, value: Optional[float]) -> Optional[timedelta]:
return timedelta(seconds=value) if value else None
class CacheConstantsField(TextField): class CacheConstantsField(TextField):
def db_value(self, value: CachingAdapter.FunctionNames) -> str: def db_value(self, value: CachingAdapter.FunctionNames) -> str:
return value.value return value.value
@@ -39,6 +32,14 @@ class CacheConstantsField(TextField):
return CachingAdapter.FunctionNames(value) return CachingAdapter.FunctionNames(value)
class DurationField(DoubleField):
def db_value(self, value: timedelta) -> Optional[float]:
return value.total_seconds() if value else None
def python_value(self, value: Optional[float]) -> Optional[timedelta]:
return timedelta(seconds=value) if value else None
class TzDateTimeField(TextField): class TzDateTimeField(TextField):
def db_value(self, value: Optional[datetime]) -> Optional[str]: def db_value(self, value: Optional[datetime]) -> Optional[str]:
return value.isoformat() if value else None return value.isoformat() if value else None
@@ -204,10 +205,13 @@ class Song(BaseModel):
class CacheInfo(BaseModel): class CacheInfo(BaseModel):
query_name = CacheConstantsField(unique=True, primary_key=True) query_name = CacheConstantsField()
params_hash = IntegerField(null=False) params_hash = TextField()
last_ingestion_time = TzDateTimeField(null=False) last_ingestion_time = TzDateTimeField(null=False)
class Meta:
primary_key = CompositeKey('query_name', 'params_hash')
class Playlist(BaseModel): class Playlist(BaseModel):
id = TextField(unique=True, primary_key=True) id = TextField(unique=True, primary_key=True)

View File

@@ -134,6 +134,9 @@ class SublimeMusicApp(Gtk.Application):
self.window.show_all() self.window.show_all()
self.window.present() self.window.present()
# Load the state for the server, if it exists.
self.app_config.load_state()
# If there is no current server, show the dialog to select a server. # If there is no current server, show the dialog to select a server.
if self.app_config.server is None: if self.app_config.server is None:
self.show_configure_servers_dialog() self.show_configure_servers_dialog()

View File

@@ -1,4 +1,5 @@
import hashlib import hashlib
import logging
import os import os
import pickle import pickle
from dataclasses import asdict, dataclass, field, fields from dataclasses import asdict, dataclass, field, fields
@@ -68,7 +69,7 @@ class ServerConfiguration:
>>> sc.strhash() >>> sc.strhash()
'6df23dc03f9b54cc38a0fc1483df6e21' '6df23dc03f9b54cc38a0fc1483df6e21'
""" """
server_info = (self.name + self.server_address + self.username) server_info = self.name + self.server_address + self.username
return hashlib.md5(server_info.encode('utf-8')).hexdigest() return hashlib.md5(server_info.encode('utf-8')).hexdigest()
@@ -100,6 +101,7 @@ class AppConfiguration:
config = AppConfiguration(**args) config = AppConfiguration(**args)
config.filename = filename config.filename = filename
return config return config
def __post_init__(self): def __post_init__(self):
@@ -136,27 +138,34 @@ class AppConfiguration:
if not server: if not server:
return UIState() return UIState()
# If already retrieved, and the server hasn't changed, then return the # If the server has changed, then retrieve the new server's state.
# state. Don't use strhash because that is much more expensive of an # TODO: if things are slow, then use a different hash
# operation. if self._current_server_hash != server.strhash():
if self._current_server_hash != hash(server) or not self._state: self.load_state()
self._current_server_hash = hash(server)
if self.state_file_location.exists():
try:
with open(self.state_file_location, 'rb') as f:
self._state = UIState(**pickle.load(f))
except Exception:
# Just ignore any errors, it is only UI state.
self._state = UIState()
# Do the import in the function to avoid circular imports.
from sublime.cache_manager import CacheManager
from sublime.adapters import AdapterManager
CacheManager.reset(self)
AdapterManager.reset(self)
return self._state return self._state
def load_state(self):
if not self.server:
return
self._current_server_hash = self.server.strhash()
if self.state_file_location.exists():
try:
with open(self.state_file_location, 'rb') as f:
self._state = UIState(**pickle.load(f))
except Exception:
logging.warning(
f"Couldn't load state from {self.state_file_location}")
# Just ignore any errors, it is only UI state.
self._state = UIState()
# Do the import in the function to avoid circular imports.
from sublime.cache_manager import CacheManager
from sublime.adapters import AdapterManager
CacheManager.reset(self)
AdapterManager.reset(self)
@property @property
def state_file_location(self) -> Path: def state_file_location(self) -> Path:
assert self.server is not None assert self.server is not None

View File

@@ -178,7 +178,7 @@ def test_caching_get_playlist_then_details(cache_adapter: FilesystemAdapter):
FilesystemAdapter.FunctionNames.GET_PLAYLISTS, FilesystemAdapter.FunctionNames.GET_PLAYLISTS,
(), (),
[ [
SubsonicAPI.Playlist('1', 'test1', comment='comment'), SubsonicAPI.Playlist('1', 'test1'),
SubsonicAPI.Playlist('2', 'test2'), SubsonicAPI.Playlist('2', 'test2'),
], ],
) )
@@ -192,3 +192,33 @@ def test_caching_get_playlist_then_details(cache_adapter: FilesystemAdapter):
assert e.partial_data assert e.partial_data
assert e.partial_data.id == '1' assert e.partial_data.id == '1'
assert e.partial_data.name == 'test1' assert e.partial_data.name == 'test1'
# Simulate getting playlist details for id=1, then id=2
songs = [
SubsonicAPI.Song(
'3',
'Song 3',
parent='foo',
album='foo',
artist='foo',
duration=timedelta(seconds=10.2),
path='/foo/song3.mp3',
),
]
cache_adapter.ingest_new_data(
FilesystemAdapter.FunctionNames.GET_PLAYLIST_DETAILS,
('1', ),
SubsonicAPI.PlaylistWithSongs('1', 'test1', songs=songs),
)
cache_adapter.ingest_new_data(
FilesystemAdapter.FunctionNames.GET_PLAYLIST_DETAILS,
('2', ),
SubsonicAPI.PlaylistWithSongs('2', 'test2', songs=songs),
)
# Going back and getting playlist details for the first one should not
# cache miss.
playlist = cache_adapter.get_playlist_details('1')
assert playlist.id == '1'
assert playlist.name == 'test1'