Skip to content

perf: cache trained ML model in learning_svc across operation completions#3328

Open
deacon-mp wants to merge 2 commits intomasterfrom
fix/learning-svc-model-cache
Open

perf: cache trained ML model in learning_svc across operation completions#3328
deacon-mp wants to merge 2 commits intomasterfrom
fix/learning-svc-model-cache

Conversation

@deacon-mp
Copy link
Contributor

Learning service retrained model from scratch after each operation. Added cache. With tests.

Add TTL-based caching to avoid unnecessary model rebuilds. Model is
only rebuilt when marked dirty or after cache TTL expires.
@deacon-mp deacon-mp requested a review from Copilot March 16, 2026 03:53
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a TTL-based cache for the LearningService ML model so it isn’t rebuilt after every operation completion, improving runtime performance while allowing explicit invalidation.

Changes:

  • Introduces _model_dirty / _model_built_at and TTL logic to skip unnecessary model rebuilds.
  • Adds invalidate_model_cache() to force a rebuild on next use.
  • Adds default config (model_cache_ttl_seconds) and unit tests covering cache behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
app/service/learning_svc.py Implements dirty+TTL cache logic and explicit invalidation for model rebuilding.
conf/default.yml Adds a configurable TTL (model_cache_ttl_seconds) for the model cache.
tests/test_learning_svc_cache.py Adds tests to validate initial state, invalidation, and rebuild/skip behavior.
Comments suppressed due to low confidence (1)

app/service/learning_svc.py:41

  • The guard checks executor.command, but the regex scan uses executor.test. If executor.test can be None/empty while executor.command is present, re.findall(...) can fail (expects a string/bytes). Either scan executor.command (if that’s the intended source) or update the guard to match the field being scanned.
                if executor.command:
                    variables = frozenset(re.findall(self.re_variable, executor.test))

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return parsers

async def build_model(self):
cache_ttl = self.get_config('model_cache_ttl_seconds') or 3600
Comment on lines 38 to 44
for ability in await self.get_service('data_svc').locate('abilities'):
for executor in ability.executors:
if executor.command:
variables = frozenset(re.findall(self.re_variable, executor.test))
if len(variables) > 1: # relationships require at least 2 variables
self.model.add(variables)
self.model = set(self.model)
Comment on lines +34 to +46
cache_ttl = self.get_config('model_cache_ttl_seconds') or 3600
if not self._model_dirty and (time.monotonic() - self._model_built_at) < cache_ttl:
self.log.debug('Skipping model rebuild - cache still valid (TTL=%ds)', cache_ttl)
return
for ability in await self.get_service('data_svc').locate('abilities'):
for executor in ability.executors:
if executor.command:
variables = frozenset(re.findall(self.re_variable, executor.test))
if len(variables) > 1: # relationships require at least 2 variables
self.model.add(variables)
self.model = set(self.model)
self._model_dirty = False
self._model_built_at = time.monotonic()
Comment on lines +31 to +35
mock_data_svc = AsyncMock()
svc.get_service = MagicMock(return_value=mock_data_svc)
svc.get_config = MagicMock(return_value=3600)
asyncio.run(svc.build_model())
mock_data_svc.locate.assert_not_called()
svc.invalidate_model_cache()
assert svc._model_dirty is True

def test_skip_rebuild_when_cache_valid(self):
- Coerce cache_ttl to int (clamped >= 1s) since get_config() may return string
- Rebuild model into a fresh local set to remove stale entries from deleted
  abilities before assigning to self.model
- Fix test: explicitly set mock_data_svc.locate = AsyncMock() before
  assert_not_called() to avoid false-positive child mock assertion
- Add test for TTL-expiry rebuild (dirty=False but TTL elapsed)
- Add test for string TTL coercion path
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants