Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6578a93
Introduce cache provider
Jul 31, 2025
11ccb21
remove debug
Jul 31, 2025
2cf5951
Rename module
Jul 31, 2025
ea17922
init cache service
Jul 31, 2025
a90c667
Flake8
Aug 1, 2025
95ede91
More changes
Aug 1, 2025
60635d5
Remove notes
Aug 1, 2025
5fafca1
More changes
Aug 1, 2025
d473f70
Use format
Aug 1, 2025
f02030a
Change to cache service
Aug 5, 2025
3f2af1c
some minor cleanup
Aug 5, 2025
302872b
Fallback only when cache provider does not exist
Aug 5, 2025
bc368ce
Call real providers inside cache providers
Aug 5, 2025
5554df3
More changes
Aug 5, 2025
1e1eade
More changes
Aug 5, 2025
66f5e53
More changes to cache provider
Aug 7, 2025
2e7d311
Update library service
Aug 7, 2025
cd662e8
Change from code review
Aug 12, 2025
86d5726
Merge branch 'master' into Feature/Cache-provider
Aug 12, 2025
85fa0a8
Typo
Aug 12, 2025
903a039
Restore comment
Aug 12, 2025
bb5f1d9
Merge branch 'master' into Feature/Cache-provider
Sep 17, 2025
5012fbc
Minor fix
Sep 18, 2025
fd63bd0
Update to cache provider
Feb 23, 2026
6054391
Merge branch 'master' into feature/Cache-provider
Feb 23, 2026
1623c02
Make cache provider fallback-safe on cache miss (return None/[]/no-op…
Feb 23, 2026
64daa54
Add debug message
Feb 23, 2026
3617df9
Add debug message after read for precise hit
Feb 24, 2026
40269eb
Add debug message after read for precise hit: warning
Feb 24, 2026
f3e0a90
Wire git provider
Apr 2, 2026
97b0143
Better wrapping.
ateska Apr 2, 2026
4d8ea2f
Merge branch 'master' into feature/Cache-provider
ateska Apr 2, 2026
3ea77bf
Add a logging of the ERRNO for inotify_init
ateska Apr 2, 2026
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
96 changes: 96 additions & 0 deletions asab/library/providers/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import os
import logging
import hashlib

from asab.config import Config
from asab.library.providers.filesystem import FileSystemLibraryProvider

L = logging.getLogger(__name__)


class CacheLibraryProvider(FileSystemLibraryProvider):
"""
A read-only cache wrapper that points at
[library:cache].dir/@global/<layer_hash>.

Any call to read()/list()/find()/subscribe() will serve from cache if present.
When cache is missing, provider methods return fallback-safe empty values.
"""

def __init__(self, library, uri, layer):
# 1) Compute the exact same layer_hash your LibraryCacheService wrote
master_hash = hashlib.sha256(uri.encode("utf-8")).hexdigest()

# 2) Locate the symlink under @global
cache_root = Config.get("library:cache", "dir", fallback=None)
if not cache_root:
raise RuntimeError("Missing [library:cache].dir configuration")
global_link = os.path.join(cache_root, "@global", master_hash)

# 3) Remember for _cache_live()
self.layer_hash = master_hash
self.cache_dir = global_link

# 4) Sanity-check cache_root exists
if not os.path.isdir(cache_root):
L.critical("Cache root '{}' not found, exiting.".format(cache_root))
raise SystemExit(1)

# 5) Warn if no snapshot directory is present yet
if not self._cache_live():
L.warning(
"No cache snapshot for URI '{}' at '{}'.".format(uri, self.cache_dir)
)

# 6) Delegate to FileSystemLibraryProvider
cache_uri = "file://{}".format(self.cache_dir.rstrip("/"))
super().__init__(library, cache_uri, layer, set_ready=False)
library.App.TaskService.schedule(self._set_ready(self._cache_live()))
library.App.PubSub.subscribe("library.cache.ready!", self._on_cache_ready)
library.App.PubSub.subscribe("library.cache.not_ready!", self._on_cache_not_ready)


async def _on_cache_ready(self, *args):
live = self._cache_live()
if live and not self.IsReady:
await self._set_ready(True)

async def _on_cache_not_ready(self, *args):
live = self._cache_live()
if (not live) and self.IsReady:
await self._set_ready(False)

def _cache_live(self):
if os.path.islink(self.cache_dir):
return os.path.isdir(os.path.realpath(self.cache_dir))
return os.path.isdir(self.cache_dir)

async def read(self, path):
if not self._cache_live():
return None

itemio = await super().read(path)
L.warning(
"Cache read %s",
"hit" if itemio is not None else "miss",
struct_data={
"path": path,
"base": self.cache_dir,
},
)
return itemio

async def list(self, path):
if not self._cache_live():
return []
return await super().list(path)

async def find(self, path):
if not self._cache_live():
return []
return await super().find(path)

async def subscribe(self, path, target=None):
if not self._cache_live():
return None
return await super().subscribe(path, target)
6 changes: 5 additions & 1 deletion asab/library/providers/filesystem.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
import os
import ctypes
import os.path
import stat
import glob
Expand Down Expand Up @@ -68,8 +69,11 @@ def __init__(self, library, path, layer, *, set_ready=True):
if inotify_init is not None:
init = inotify_init()
if init == -1:
err = ctypes.get_errno()
L.warning(
"Subscribing to library changes in filesystem provider is not available. Inotify was not initialized.")
"Subscribing to library changes in filesystem provider is not available. Inotify was not initialized.",
struct_data={'errno': err},
)
self.FD = None
else:
self.FD = init
Expand Down
56 changes: 38 additions & 18 deletions asab/library/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,37 +120,56 @@ async def _on_tick60(self, message_type):
await self._read_disabled()
await self._read_favorites()


def _create_library(self, path, layer):
library_provider = None

if path.startswith('libsreg+'):
from .providers.libsreg import LibsRegLibraryProvider
realp = LibsRegLibraryProvider(self, path, layer)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like having two layers here:

  1. in case of some race condition or mistake, when the two layers are not the same, this might bring weird mistakes
  2. we want to cut off number of http requests from the whole installation and this just adds cache provider on the top of the libsreg provider - no cut in the network traffic

We agreed with Mithun to start with a simple check in the init time:
->If cache is not present (can be chceked in init time), start and use libsreg provider.

There can be scenarios that break that:

  • cache is gonna be ready 30s after init time and service never switches to cache.
  • cache can disappear in the runtime and service doesn't fallback to libsreg.

Let's start with something, make sure it survives an upgrade procedure and only after then solve these scenarios/conditions?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During the upgrade,
new version of library will be downloaded to the cache as soon as possible. However, the services will be still running, requiring the old library, until they are upgraded.
We need to have a secure, well designed solution for this race condition

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the right approach is to use init only for initial state, then keep liveness checks and readiness updates at runtime, so we can fall back to libsreg when cache is unavailable and automatically use cache again when it becomes live.

if 'library:cache' in Config:
from .providers.cache import CacheLibraryProvider
cachep = CacheLibraryProvider(self, path, layer)
self.Libraries.append(cachep)
else:
self.Libraries.append(realp)
return

elif path.startswith('git+'):
from .providers.git import GitLibraryProvider
realp = GitLibraryProvider(self, path, layer)
if 'library:cache' in Config:
from .providers.cache import CacheLibraryProvider
cachep = CacheLibraryProvider(self, path, layer)
self.Libraries.append(cachep)
else:
self.Libraries.append(realp)
return

# ZooKeeper (no cache support)
if path.startswith('zk://') or path.startswith('zookeeper://'):
from .providers.zookeeper import ZooKeeperLibraryProvider
library_provider = ZooKeeperLibraryProvider(self, path, layer)
provider = ZooKeeperLibraryProvider(self, path, layer)
self.Libraries.append(provider)

# Filesystem (no cache support)
elif path.startswith('./') or path.startswith('/') or path.startswith('file://'):
from .providers.filesystem import FileSystemLibraryProvider
library_provider = FileSystemLibraryProvider(self, path, layer)
provider = FileSystemLibraryProvider(self, path, layer)
self.Libraries.append(provider)

# Azure Storage
elif path.startswith('azure+https://'):
from .providers.azurestorage import AzureStorageLibraryProvider
library_provider = AzureStorageLibraryProvider(self, path, layer)

elif path.startswith('git+'):
from .providers.git import GitLibraryProvider
library_provider = GitLibraryProvider(self, path, layer)
provider = AzureStorageLibraryProvider(self, path, layer)
self.Libraries.append(provider)

elif path.startswith('libsreg+'):
from .providers.libsreg import LibsRegLibraryProvider
library_provider = LibsRegLibraryProvider(self, path, layer)

elif path == '' or path.startswith("#") or path.startswith(";"):
# This is empty or commented line
# comments or blanks
elif not path or path[0] in ('#', ';'):
return

else:
L.error("Incorrect/unknown provider for '{}'".format(path))
raise SystemExit("Exit due to a critical configuration error.")

self.Libraries.append(library_provider)
L.error("Incorrect provider for '{}'".format(path))
raise SystemExit(1)

def is_ready(self) -> bool:
"""
Expand Down Expand Up @@ -372,6 +391,7 @@ async def list(self, path: str = "/", recursive: bool = False, timeout: int = No

return items


async def _list(self, path, providers):
"""
Lists items from all providers, merging items with the same name,
Expand Down
Loading