Conversation
Building the schema scans the redis graph for its node types and a summary.
Measured against a 76M edge FalkorDB graph that scan takes 833s -- the
`MATCH (c) RETURN DISTINCT labels(c) as types, count(c) as count` summary
alone is 99s -- and it is paid per worker process, because
SchemaFactory._cached is a class attribute. The refresh thread then repeats
it every update_interval, so with the hardcoded 20 minutes each worker spent
roughly 40% of its life rebuilding, indefinitely. A worker's first request
pays it twice concurrently: __init__ builds synchronously while the thread
it just started immediately builds again.
None of that work is read by a query naming a `redis:<graph>` source.
SelectStatement.execute reads only
schema.config['schema'][key]['redis_connection_params'], which comes from
schema.yaml, then hands the query graph to PLATER's GraphInterface. The
scanned type map lives in schema.schema, which that path never touches.
A skip_redis flag already existed but was unreachable and broken:
- Unreachable: TranQL.__init__ constructed SchemaFactory without it, so it
was always the default False.
- Broken: the scan is also what sets metadata['schema'], and that key is
read unconditionally later in the same loop, so skip_redis=True raised
KeyError('schema') for any redis backed entry. It presumably only ever
ran against configs with no redis schema. Skipping now leaves an empty
type map instead of an absent key.
So this wires SKIP_REDIS_SCHEMA through and makes it actually work.
Default stays false, preserving current behaviour; deployments whose
queries all name a redis source can turn it on. Note TranQL's own /search
endpoint infers default indexes from the scanned schema (api.py), so that
endpoint does depend on it.
SCHEMA_UPDATE_INTERVAL is exposed at the same time, because 20 minutes is
only safe relative to how long the scan takes and 1200s against an 833s
build is not.
Booleans out of Config need coercing: Config.__getitem__ returns
environment overrides verbatim, so SKIP_REDIS_SCHEMA=false arrives as the
string "false" and bool("false") is True. util.as_bool handles that and
raises on anything it cannot interpret, rather than silently picking a
branch. The existing flags read via Config.get have the same latent issue
if set from the environment; not changed here.
Tests: 22 covering as_bool's string forms, that skip_redis=True does not
construct RedisAdapter while still loading the connection params, and that
skip_redis=False still does scan, so the fix cannot silently disable it for
everyone. tests/test_tranql.py is 8 failed / 41 passed both before and
after this change on origin/develop.
The previous commit defaulted metadata['schema'] so the skip path stopped raising KeyError, but that only moved the crash. add_layer calls decorate_schema, which scores edges from adapter.summary and gets the adapter via RedisAdapter._get_adapter(name). The registration it looks up happens in set_adapter, inside the branch skip_redis skips, so _get_adapter raised: ValueError: Redis backend with name redis not registered. Caught against the real schema.yaml on a deployed image, not by the tests: the fixture in the previous commit had an empty type map, so add_layer iterated nothing and decorate_schema was never reached. decorate_schema also returns early unless the entry is named exactly 'redis', which hid it further. The summary being decorated exists only because the scan ran, so with the scan skipped there is nothing to score and leaving the schema undecorated is the correct outcome, not a workaround. Guarded on the registry rather than threading skip_redis down, so any caller reaching decorate_schema without a registered adapter degrades instead of raising. Test now uses a non-empty layer under an entry named 'redis', which is what it takes to reach this code, and asserts no adapter is constructed while the connection params still load.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Building the schema scans the redis graph for node types and a summary. Measured against a 76M-edge FalkorDB graph:
MATCH (c) RETURN DISTINCT labels(c) as types, count(c) as countThree multipliers on top of that:
SchemaFactory._cachedis a class attribute, so the build is per worker process.update_interval, hardcoded to20*60. An 833 s build on a 1200 s sleep means each worker spends ~40% of its life rebuilding the schema, indefinitely.SchemaFactory.__init__builds synchronously, while theupdate_cache_loopthread it just started immediately builds again.Observed effect: raising gunicorn from 1 to 16 workers made a client crawl make zero progress for 30+ minutes, with the tranql pod pinned at its 2-core limit and redis idle at 7m.
None of that scan is used by
redis:queriesSelectStatement.executereads only:That's
schema.config, loaded fromschema.yaml. It then hands the query graph to PLATER'sGraphInterface.answer_trapi_question. The scanned type map lives inschema.schema, which this path never reads —generate_questionsuses the parsed query'sconcept.type_name/concept.curies, andplan()/execute_plan()is the/schemabranch.skip_redisalready existed, but was unreachable and brokenTranQL.__init__constructedSchemaFactory(...)without it, so it was always the defaultFalse.metadata['schema'](line 309), andschema_data = metadata['schema']is read unconditionally later in the same loop (line 322). Soskip_redis=TrueraisedKeyError('schema')for any redis-backed entry — it can only ever have run against configs with no redis schema. My test caught this; skipping now leaves an empty type map rather than an absent key.Change
SKIP_REDIS_SCHEMAthroughTranQL.__init__→SchemaFactory→Schema, and make the skip path coherent.SCHEMA_UPDATE_INTERVAL, because 20 minutes is only safe relative to how long the scan takes.util.as_bool.Config.__getitem__returns environment overrides verbatim, soSKIP_REDIS_SCHEMA=falsearrives as the string"false"— andbool("false")isTrue. Without coercion the off switch would mean on. It raises on anything uninterpretable rather than silently picking a branch.Default stays
false, so current behaviour is preserved. Deployments whose queries all name aredis:source can turn it on.Caveat
TranQL's own /search endpoint infers its default indexes from the scanned schema (
api.py:schema.schema[redis_schema_name]["schema"].keys()). EnablingSKIP_REDIS_SCHEMAleaves that empty, so it should stay off for any deployment using /search. The query endpoint is unaffected.The other flags read via
Config.get(ASYNCHRONOUS_REQUESTS,NAME_BASED_MERGING, …) have the same latent string-boolean issue if set from the environment. Not touched here to keep the diff reviewable.Tests
tests/test_skip_redis_schema.py, 22 tests, no redis or graph needed:as_boolacross"false"/"False"/"0"/"no"/"off"/""and the truthy forms, real bools,None+ default, and that garbage raisesskip_redis=Truedoes not constructRedisAdapter, whileconfig['schema'][...]['redis_connection_params']still loads — that's what the query path actually readsskip_redis=Falsestill constructs it, so this fix cannot silently disable the scan for everyonetests/test_tranql.pyis 8 failed / 41 passed both before and after, verified by running the same suite against a pristineorigin/developtree — the same eight test names fail either way, so they are pre-existing.