Skip to content

Conversation

YuriZmytrakov
Copy link
Collaborator

@YuriZmytrakov YuriZmytrakov commented Oct 8, 2025

Related Issue(s):

Description:

Add Redis caching support for navigation pagination to enable proper prev/next links in STAC API responses.

PR Checklist:

  • Code is formatted and linted (run pre-commit run --all-files)
  • Tests pass (run make test)
  • Documentation has been updated to reflect changes, if applicable
  • Changes are added to the changelog

@YuriZmytrakov YuriZmytrakov force-pushed the CAT-1382-2 branch 9 times, most recently from 5c8d201 to 8991cfc Compare October 8, 2025 13:07
@YuriZmytrakov YuriZmytrakov changed the title feat: add redis cache for navigation feat: Add Redis caching for navigation pagination Oct 8, 2025
@YuriZmytrakov YuriZmytrakov force-pushed the CAT-1382-2 branch 2 times, most recently from b16cc88 to 8991cfc Compare October 8, 2025 13:30

if redis_enable:
try:
redis = await connect_redis()
Copy link
Collaborator

Choose a reason for hiding this comment

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

how does this work with connect_redis_sentinel?

Copy link
Collaborator

Choose a reason for hiding this comment

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

there is connect_redis and connect_redis_sentinel. I think maybe there is logic missing which should determine which one to use? Maybe you can explain how it works?

Copy link
Collaborator

Choose a reason for hiding this comment

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

we should also log the redis connection whether its successful or not?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  1. If a user wants to use standalone Redis (the most common case), they should use connect_redis(). If they want to use Redis Sentinel (used by Cloudferro), connect_redis_sentinel() should be used. The connection is configured differently in each case, which is why we have two separate functions. The user needs to configure either the REDIS or REDIS_SENTINEL environment variables.

  2. The selection of which function to use should be done in redis_utils.py:

    • For standalone Redis: redis_settings: BaseSettings = RedisSettings()
    • For Redis Sentinel: redis_settings: BaseSettings = RedisSentinelSettings()
  3. Logging has been added for users.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok thanks for explaining - here you only use connect_redis but should you use connect_redis_sentinel as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If the user configures Redis Sentinel, the connect_redis_sentinel function should be used instead of connect_redis. I could try to combine these two functions into one, which would choose the appropriate approach depending on whether the user wants standalone or Sentinel depending on config selected. This way, no one would need to manually replace function, only connect_redis would be called. Both approaches are fine with me. Just to note, Cloudferro will probably be the only users of Sentinel, which is why I added this function.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good idea, maybe even a helper function that calls the right function. If there are env options for sentinel in the codebase then we have to make it useable. Never say never.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jonhealy1 this has been implemented. Moving forward, depending on the configuration specified via environment variables, it will be handled by connect_redis. The developer is not expected to select the function.

"href": prev_link,
},
)

Copy link
Collaborator

@jonhealy1 jonhealy1 Oct 10, 2025

Choose a reason for hiding this comment

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

can this code - the redis_enabled block - be put into a function? It is used with all_collections too

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The code block was replaced here with func as well. Thank you!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's change the function name for handle_pagination_links - also, mentioned below - and let's check the REDIS IS ENABLED here before we go to the function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done, thank you!

"href": prev_link,
},
)

Copy link
Collaborator

Choose a reason for hiding this comment

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

this code into a function

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jonhealy1 I have moved this code into handle_pagination_links func that can be reused where needed.

Yuri Zmytrakov added 3 commits October 10, 2025 14:18
- Configure Redis image for tests of caching navigation
- Update Make file Redis with test targets for ES and OS
- Integrate Redis/Redis Sentinel client to cache navigation
- Add Redis funcs Sentinel for navigation caching
Add tests for Redis pagination caching in search and collections endpoints, plus utility function tests.
@YuriZmytrakov YuriZmytrakov force-pushed the CAT-1382-2 branch 8 times, most recently from f8a4ad9 to ff38a98 Compare October 12, 2025 20:27
Copy link
Collaborator

@jamesfisher-geo jamesfisher-geo left a comment

Choose a reason for hiding this comment

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

Looks good. Thanks @YuriZmytrakov

@jonhealy1
Copy link
Collaborator

@YuriZmytrakov Let us know when this has been approved by QA

@YuriZmytrakov
Copy link
Collaborator Author

@YuriZmytrakov Let us know when this has been approved by QA

This PR has passed QA and is ready to be merged.

@YuriZmytrakov
Copy link
Collaborator Author

Nice work. I think we need to document this better in the README, ie. have instructions on how to install. I can open a separate issue to add this. Really cool having prev pagination links.

I have added the installation steps for navigation via the Redis cache. The steps are straightforward and can be expanded once Redis is used for caching additional navigation links.

Copy link
Collaborator

@StijnCaerts StijnCaerts left a comment

Choose a reason for hiding this comment

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

Nice feature addition! Just be careful with potential cache conflicts, as highlighted in the code comment.

Comment on lines 221 to 267
async def save_self_link(
redis: aioredis.Redis, token: Optional[str], self_href: str
) -> None:
"""Save the self link for the current token."""
if token:
if sentinel_settings.REDIS_SENTINEL_HOSTS:
ttl_seconds = sentinel_settings.REDIS_SELF_LINK_TTL
elif standalone_settings.REDIS_HOST:
ttl_seconds = standalone_settings.REDIS_SELF_LINK_TTL
await redis.setex(f"nav:self:{token}", ttl_seconds, self_href)


async def get_prev_link(redis: aioredis.Redis, token: Optional[str]) -> Optional[str]:
"""Get the previous page link for the current token."""
if not token:
return None
return await redis.get(f"nav:self:{token}")


async def redis_pagination_links(
current_url: str, token: str, next_token: str, links: list
) -> None:
"""Handle Redis pagination."""
redis = await connect_redis()
if not redis:
logger.warning("Redis connection failed.")
return

try:
if next_token:
await save_self_link(redis, next_token, current_url)

prev_link = await get_prev_link(redis, token)
if prev_link:
links.insert(
0,
{
"rel": "previous",
"type": "application/json",
"method": "GET",
"href": prev_link,
},
)
except Exception as e:
logger.warning(f"Redis pagination operation failed: {e}")
finally:
await redis.close()
Copy link
Collaborator

Choose a reason for hiding this comment

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

By only storing the token and not the full URL in the key, we might hit unwanted cache conflicts. A pagination token might occur in different search queries. For example, these 2 searches with different queries return the same pagination token.

https stac.terrascope.be/search limit==1 collection==sentinel-2-l1c | jq '.links[] | select(.rel=="next") | .href'
"https://stac.terrascope.be/search?limit=1&collection=sentinel-2-l1c&token=WzE3NjEwMzAxMjkwMjQsIlMyQl9NU0lMMUNfMjAyNTEwMjFUMDcwMjA5X04wNTExX1IxMjBfVDM4S1FEXzIwMjUxMDIxVDA5NTkyOSIsInNlbnRpbmVsLTItbDFjIl0%3D"https stac.terrascope.be/search limit==1 | jq '.links[] | select(.rel=="next") | .href'
"https://stac.terrascope.be/search?limit=1&token=WzE3NjEwMzAxMjkwMjQsIlMyQl9NU0lMMUNfMjAyNTEwMjFUMDcwMjA5X04wNTExX1IxMjBfVDM4S1FEXzIwMjUxMDIxVDA5NTkyOSIsInNlbnRpbmVsLTItbDFjIl0%3D"

Retrieving the previous link from the cache would potentially change the search query in this case. The links will also only work for GET searches, so best to explicitly limit this and do not try to retrieve the link for POST searches.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for the heads up. This has been tested, no collisions have occurred. I don’t see any issues with replacing the token with the full URL as the key to add or retrieve the previous page URL from the Redis cache for navigation. However, please note that we are using the proxy, and if the full URL is used, the keys will become irrelevant. As a solution, I have added additional parameters from the URL endpoint + token. This prevents accidentally retrieving the wrong previous page URL, as different requests might share the same token. In this case, the following Redis keys will be created:

nav:search?limit=1&collection=sentinel-2-l1c&token=WzE3NjEwMzAxMjkwMjQsIlMyQl9NU0lMMUNfMjAyNTEwMjFUMDcwMjA5X04wNTExX1IxMjBfVDM4S1FEXzIwMjUxMDIxVDA5NTkyOSIsInNlbnRpbmVsLTItbDFjIl0%3D"

nav:/search?limit=1&token=WzE3NjEwMzAxMjkwMjQsIlMyQl9NU0lMMUNfMjAyNTEwMjFUMDcwMjA5X04wNTExX1IxMjBfVDM4S1FEXzIwMjUxMDIxVDA5NTkyOSIsInNlbnRpbmVsLTItbDFjIl0%3D"

Also, the POST requests are excluded from retrieving the navigation links.

@YuriZmytrakov YuriZmytrakov force-pushed the CAT-1382-2 branch 4 times, most recently from d409c95 to 1e492b6 Compare October 21, 2025 20:54
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.

5 participants