Skip to content

Commit

Permalink
Implement a local on-disk cache mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
justvanrossum committed Dec 3, 2023
1 parent cf2e00d commit 6f24027
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 11 deletions.
54 changes: 47 additions & 7 deletions src/fontra_rcjk/backend_mysql.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import json
import logging
import traceback
from copy import deepcopy
Expand All @@ -7,7 +8,7 @@
from random import random

from fontra.backends.designspace import makeGlyphMapChange
from fontra.core.classes import unstructure
from fontra.core.classes import VariableGlyph, structure, unstructure
from fontra.core.instancer import mapLocationFromUserToSource

from .base import (
Expand Down Expand Up @@ -46,10 +47,14 @@ class RCJKGlyphInfo:

class RCJKMySQLBackend:
@classmethod
def fromRCJKClient(cls, client, fontUID):
def fromRCJKClient(cls, client, fontUID, cacheDir=None):
self = cls()
self.client = client
self.fontUID = fontUID
if cacheDir is not None:
cacheDir = cacheDir / fontUID
cacheDir.mkdir(exist_ok=True, parents=True)
self.cacheDir = cacheDir
self.pollExternalChangesInterval = 10
self._rcjkGlyphInfo = None
self._glyphCache = LRUCache()
Expand All @@ -75,6 +80,8 @@ async def _ensureGlyphMap(self):
rcjkGlyphInfo = {}
glyphMap = {}
response = await self.client.glif_list(self.fontUID)
if self._lastPolledForChanges is None:
self._lastPolledForChanges = response["server_datetime"]
for typeCode, typeName in _glyphTypes:
for glyphInfo in response["data"][typeName]:
glyphMap[glyphInfo["name"]] = _unicodesFromGlyphInfo(glyphInfo)
Expand Down Expand Up @@ -139,12 +146,42 @@ async def putCustomData(self, customData):
self._tempFontItemsCache["customData"] = deepcopy(customData)
_ = await self.client.font_update(self.fontUID, fontlib=customData)

def _readGlyphFromCacheDir(self, glyphName):
if self.cacheDir is None:
return None
glyphInfo = self._rcjkGlyphInfo[glyphName]
fileName = f"{glyphInfo.glyphID}-{glyphInfo.updated}.json"
path = self.cacheDir / fileName
if not path.exists():
return None
return structure(json.loads(path.read_text(encoding="utf-8")), VariableGlyph)

def _writeGlyphToCacheDir(self, glyphName, glyph):
if self.cacheDir is None:
return
glyphInfo = self._rcjkGlyphInfo[glyphName]
globPattern = f"{glyphInfo.glyphID}-*.json"
for stalePath in self.cacheDir.glob(globPattern):
stalePath.unlink()
fileName = f"{glyphInfo.glyphID}-{glyphInfo.updated}.json"
path = self.cacheDir / fileName
try:
path.write_text(
json.dumps(unstructure(glyph), separators=(",", ":")), encoding="utf-8"
)
except Exception as e:
logger.error("error writing to local cache: %r", e)

async def getGlyph(self, glyphName):
await self._ensureGlyphMap()
if glyphName not in self._glyphMap:
return None
layerGlyphs = await self._getLayerGlyphs(glyphName)
return buildVariableGlyphFromLayerGlyphs(layerGlyphs)
glyph = self._readGlyphFromCacheDir(glyphName)
if glyph is None:
layerGlyphs = await self._getLayerGlyphs(glyphName)
glyph = buildVariableGlyphFromLayerGlyphs(layerGlyphs)
self._writeGlyphToCacheDir(glyphName, glyph)
return glyph

async def _getLayerGlyphs(self, glyphName):
layerGlyphs = self._glyphCache.get(glyphName)
Expand Down Expand Up @@ -261,6 +298,7 @@ async def _putGlyph(self, glyphName, glyph, unicodes):
timeStamp = getUpdatedTimeStamp(unlockResponse["data"])
self._glyphTimeStamps[glyphName] = timeStamp
self._rcjkGlyphInfo[glyphName].updated = timeStamp
self._writeGlyphToCacheDir(glyphName, glyph)

return errorMessage

Expand Down Expand Up @@ -385,12 +423,14 @@ async def _pollOnceForChanges(self):
if glyphName not in self._rcjkGlyphInfo:
assert glyphName not in self._glyphMap
logger.info(f"New glyph {glyphName}")
self._rcjkGlyphInfo[glyphName] = RCJKGlyphInfo(
typeCode, glyphInfo["id"], glyphUpdatedAt
)
else:
assert glyphName in self._glyphMap, f"glyph not found: {glyphName}"

self._glyphTimeStamps[glyphName] = glyphUpdatedAt
self._rcjkGlyphInfo[glyphName] = RCJKGlyphInfo(
typeCode, glyphInfo["id"], glyphUpdatedAt
)

unicodes = _unicodesFromGlyphInfo(glyphInfo)
if unicodes != self._glyphMap.get(glyphName):
self._glyphMap[glyphName] = unicodes
Expand Down
18 changes: 14 additions & 4 deletions src/fontra_rcjk/projectmanager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import pathlib
import secrets
from importlib import resources
from urllib.parse import parse_qs, quote
Expand All @@ -18,19 +19,25 @@ class RCJKProjectManagerFactory:
def addArguments(parser):
parser.add_argument("rcjk_host")
parser.add_argument("--read-only", action="store_true")
parser.add_argument("--cache-dir")

@staticmethod
def getProjectManager(arguments):
return RCJKProjectManager(
host=arguments.rcjk_host,
readOnly=arguments.read_only,
cacheDir=arguments.cache_dir,
)


class RCJKProjectManager:
def __init__(self, host, *, readOnly=False):
def __init__(self, host, *, readOnly=False, cacheDir=None):
self.host = host
self.readOnly = readOnly
if cacheDir is not None:
cacheDir = pathlib.Path(cacheDir).resolve()
cacheDir.mkdir(exist_ok=True)
self.cacheDir = cacheDir
self.authorizedClients = {}

async def close(self):
Expand Down Expand Up @@ -116,7 +123,7 @@ async def login(self, username, password):
logger.info(f"successfully logged in '{username}'")
token = secrets.token_hex(32)
self.authorizedClients[token] = AuthorizedClient(
rcjkClient, readOnly=self.readOnly
rcjkClient, readOnly=self.readOnly, cacheDir=self.cacheDir
)
return token

Expand All @@ -143,9 +150,10 @@ async def getRemoteSubject(self, path, token):


class AuthorizedClient:
def __init__(self, rcjkClient, readOnly=False):
def __init__(self, rcjkClient, readOnly=False, cacheDir=None):
self.rcjkClient = rcjkClient
self.readOnly = readOnly
self.cacheDir = cacheDir
self.projectMapping = None
self.fontHandlers = {}

Expand Down Expand Up @@ -177,7 +185,9 @@ async def getFontHandler(self, path):
fontHandler = self.fontHandlers.get(path)
if fontHandler is None:
_, fontUID = self.projectMapping[path]
backend = RCJKMySQLBackend.fromRCJKClient(self.rcjkClient, fontUID)
backend = RCJKMySQLBackend.fromRCJKClient(
self.rcjkClient, fontUID, self.cacheDir
)

async def closeFontHandler():
logger.info(f"closing FontHandler '{path}' for '{self.username}'")
Expand Down

0 comments on commit 6f24027

Please sign in to comment.