Skip to content

backend/inmem-cleanup-rel-imprv#1333

Merged
piyushKumar-1 merged 1 commit into
mainfrom
logging-imprv
Jun 6, 2026
Merged

backend/inmem-cleanup-rel-imprv#1333
piyushKumar-1 merged 1 commit into
mainfrom
logging-imprv

Conversation

@piyushKumar-1
Copy link
Copy Markdown
Member

@piyushKumar-1 piyushKumar-1 commented Jun 6, 2026

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates

Description

Additional Changes

  • This PR modifies the database schema (database migration added)
  • This PR modifies dhall configs/environment variables

Motivation and Context

How did you test it?

Checklist

  • I formatted the code and addressed linter errors ./dev/format-all-files.sh
  • I reviewed submitted code
  • I added unit tests for my changes where possible
  • I added a CHANGELOG entry if applicable

Summary by CodeRabbit

  • New Features

    • Cache refresh responses now include per-pod cleanup acknowledgment with success/failure status and error details.
  • Improvements

    • Implemented automatic retry mechanism for cache operations with exponential backoff (up to 3 attempts).
    • Enhanced cache cleanup logic to separately handle TTL expiration followed by size-based eviction, improving cache efficiency and reliability.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 6, 2026

Need the big picture first? Review this PR in Change Stack to see what changed before going file by file.

Review Change Stack

Walkthrough

This PR enhances in-memory cache management with cross-pod coordination through Redis notifications. Type contracts are extended to track cleanup notifications, the core cleanup thread separates TTL expiry from size-based eviction, handler APIs are updated to send Redis notifications on cache refresh, and HTTP client resilience is added to retry on connection failures.

Changes

Cross-pod cache cleanup coordination via Redis

Layer / File(s) Summary
Type contracts and serialization
lib/mobility-core/src/Kernel/Types/CacheFlow.hs, lib/mobility-core/src/Kernel/Storage/InMem/Management/Types.hs
ForceCleanupExpiryValue gains ToJSON derivation. InMemRefreshResponse adds redisNotified :: Bool, SidecarRefreshResponse restructures to include service counters and a pods list of PodCleanupAck records, which serialize errorMessage as error via custom Aeson options.
HTTP client retry resilience
lib/mobility-core/src/Kernel/Storage/InMem/Management/SidecarClient.hs
callWithRetry helper retries on ConnectionError with exponential backoff (100ms, 200ms, 400ms, …) for up to 3 attempts. Both callRegisterKey and callRefresh now wrap runClientM with this retry logic.
Core cache refresh and cleanup with Redis coordination
lib/mobility-core/src/Kernel/Storage/InMem.hs
refreshInMem writes a Redis force-cleanup key using current time and key-infix for cross-pod coordination. The cleanup thread refactors eviction into (1) TTL-expired removal with tracking, then (2) size-based LRU eviction retaining entries up to ~75% of capacity, with cache writeback triggered by either cleanup type or force-cleanup flags.
Handler layer cache refresh API with Redis notification
lib/mobility-core/src/Kernel/Storage/InMem/Management/Handler.hs
refreshCache computes deleted/remaining counts via a single case expression, then always sends a Redis notification expiry value (inmem:force:cleanup:timeofday) derived from current time and optional key-infix, always returning redisNotified = True.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Hopping through the cache with Redis in tow,
We coordinate the cleanup, let all the pods know,
TTL and size-based, now neatly aligned,
With retries and backoff, no cache left behind!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'backend/inmem-cleanup-rel-imprv' is vague and uses abbreviations (rel-imprv) that obscure the actual changes being made to the codebase. Consider using a clearer title that describes the main changes, such as 'Improve in-memory cache cleanup with Redis synchronization and LRU eviction' or 'Refactor in-memory cache eviction logic and add Redis notification'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch logging-imprv

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/mobility-core/src/Kernel/Storage/InMem.hs (1)

165-183: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This force-cleanup flag becomes sticky on long-lived pods.

The check is keyed off updatedCacheInfo.createdAt, which is cache-wide and usually much older than the refresh notification. Once the condition becomes true, expire key 600 renews the Redis marker on every cleanup pass, so matching entries keep getting removed every minute even after they were recached. Compare against per-entry creation timestamps or consume/delete the marker after one successful sweep instead of extending its TTL in the loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/Storage/InMem.hs` around lines 165 - 183, The
current cleanup renews the Redis marker by calling Hedis.expire key 600 inside
the cache-wide createdAt check, making the force-cleanup sticky; fix by NOT
renewing the marker in the cleanup loop and either (A) compare each entry's own
creation timestamp (e.g. use InMemKeyInfo.createdAt or similar per-entry time on
v in the fold) against forceCleanupTimestamp (from forceCleanupValue) to decide
removal, or (B) perform the cache-wide sweep once and then remove/consume the
marker (Hedis.del key) instead of calling Hedis.expire; also remove the
Hedis.expire key 600 call from this branch so matching entries are not
repeatedly re-removed. Ensure references: updatedCacheInfo,
forceCleanupTimestamp, forceCleanupKeyPrefix, InMemKeyInfo, InMemCacheInfo, and
the Hedis.expire key call are updated accordingly.
lib/mobility-core/src/Kernel/Types/CacheFlow.hs (1)

94-98: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Store a full timestamp here, not TimeOfDay.

This value is written by the refresh paths and later compared against cache state in lib/mobility-core/src/Kernel/Storage/InMem.hs. Dropping the date means a refresh near UTC midnight can be skipped on other pods because 23:59 and 00:05 are no longer orderable once the day is lost. Use UTCTime or another absolute timestamp in the Redis payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/Types/CacheFlow.hs` around lines 94 - 98, The
struct ForceCleanupExpiryValue currently uses TimeOfDay for
forceCleanupTimestamp which loses date information; change forceCleanupTimestamp
to UTCTime (Data.Time.Clock) and update its FromJSON/ToJSON usage (or derive
instances) so the Redis payload stores an absolute timestamp; then update all
places that construct or compare ForceCleanupExpiryValue (e.g., refresh paths
that create it and the comparison logic in InMem.hs that orders the timestamp)
to produce/get UTCTime (use getCurrentTime when creating) and to compare UTCTime
values instead of TimeOfDay, and add the necessary import for
UTCTime/getCurrentTime.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/mobility-core/src/Kernel/Storage/InMem.hs`:
- Around line 108-115: Both refresh paths currently overwrite the single Redis
key "inmem:force:cleanup:timeofday" (setExp) causing missed invalidations;
change the design to append events instead of replacing them: update the writers
(the code that builds ForceCleanupExpiryValue and calls
runInMultiCloudRedisWrite setExp "inmem:force:cleanup:timeofday") to push the
payload into a Redis append structure (e.g., LPUSH to
"inmem:force:cleanup:events" or XADD to a stream) including
forceCleanupTimestamp and forceCleanupKeyPrefix/keyInfix, and update
inMemCleanupThread to consume all queued events (BRPOP/LPOP loop or
XREAD/XREADGROUP) and process each payload rather than reading one shared key;
ensure the event list/stream has retention (trim/expire) so old entries are
removed.

In `@lib/mobility-core/src/Kernel/Storage/InMem/Management/Handler.hs`:
- Around line 78-92: The refresh path currently does a non-atomic readIORef then
writeIORef on inMemHashMap (inside refreshInMem handler using req.keyInfix),
which can lose concurrent inserts; change the logic to use atomicModifyIORef so
the snapshot and update happen atomically: perform the filter/clear and
recompute cacheSize inside an atomicModifyIORef on (inMemHashMap inMemEnv),
returning the new InMemCacheInfo and the deleted/remaining counts; reference the
existing InMemCacheInfo type, the inMemHashMap IORef, req.keyInfix, and keep
consistency with how refreshInMem already uses atomicModifyIORef and how
withInMemCache performs concurrent inserts.

In `@lib/mobility-core/src/Kernel/Storage/InMem/Management/Types.hs`:
- Around line 70-85: The OpenAPI schema for PodCleanupAck is out-of-sync with
the Aeson JSON rename: podCleanupAckJsonOptions maps errorMessage → "error" for
ToJSON/FromJSON but PodCleanupAck still derives ToSchema (so the schema will
expose "errorMessage"); update this by providing an explicit ToSchema
PodCleanupAck that uses the same Aeson options (e.g., use
genericDeclareUnNamedSchema/fromAesonOptions with podCleanupAckJsonOptions) so
the generated schema matches the runtime JSON keys (affecting PodCleanupAck and
SidecarRefreshResponse.pods), or alternatively remove the rename in
podCleanupAckJsonOptions so schema and JSON already align.

---

Outside diff comments:
In `@lib/mobility-core/src/Kernel/Storage/InMem.hs`:
- Around line 165-183: The current cleanup renews the Redis marker by calling
Hedis.expire key 600 inside the cache-wide createdAt check, making the
force-cleanup sticky; fix by NOT renewing the marker in the cleanup loop and
either (A) compare each entry's own creation timestamp (e.g. use
InMemKeyInfo.createdAt or similar per-entry time on v in the fold) against
forceCleanupTimestamp (from forceCleanupValue) to decide removal, or (B) perform
the cache-wide sweep once and then remove/consume the marker (Hedis.del key)
instead of calling Hedis.expire; also remove the Hedis.expire key 600 call from
this branch so matching entries are not repeatedly re-removed. Ensure
references: updatedCacheInfo, forceCleanupTimestamp, forceCleanupKeyPrefix,
InMemKeyInfo, InMemCacheInfo, and the Hedis.expire key call are updated
accordingly.

In `@lib/mobility-core/src/Kernel/Types/CacheFlow.hs`:
- Around line 94-98: The struct ForceCleanupExpiryValue currently uses TimeOfDay
for forceCleanupTimestamp which loses date information; change
forceCleanupTimestamp to UTCTime (Data.Time.Clock) and update its
FromJSON/ToJSON usage (or derive instances) so the Redis payload stores an
absolute timestamp; then update all places that construct or compare
ForceCleanupExpiryValue (e.g., refresh paths that create it and the comparison
logic in InMem.hs that orders the timestamp) to produce/get UTCTime (use
getCurrentTime when creating) and to compare UTCTime values instead of
TimeOfDay, and add the necessary import for UTCTime/getCurrentTime.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83f092ab-0b09-4533-b7d7-8605cd3ea50a

📥 Commits

Reviewing files that changed from the base of the PR and between 01ed27b and 92306ab.

📒 Files selected for processing (5)
  • lib/mobility-core/src/Kernel/Storage/InMem.hs
  • lib/mobility-core/src/Kernel/Storage/InMem/Management/Handler.hs
  • lib/mobility-core/src/Kernel/Storage/InMem/Management/SidecarClient.hs
  • lib/mobility-core/src/Kernel/Storage/InMem/Management/Types.hs
  • lib/mobility-core/src/Kernel/Types/CacheFlow.hs

Comment on lines +108 to +115
-- Write to Redis forceCleanup key so other pods also clean up
let cleanupTimeOfDay = timeToTimeOfDay (utctDayTime now)
val =
ForceCleanupExpiryValue
{ forceCleanupTimestamp = cleanupTimeOfDay,
forceCleanupKeyPrefix = Just keyInfix
}
runInMultiCloudRedisWrite $ setExp "inmem:force:cleanup:timeofday" val 600
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

A single Redis marker will drop refresh events.

Both refresh paths write the same inmem:force:cleanup:timeofday key, while inMemCleanupThread polls once per minute and reads only one payload. If two refreshes happen before the next poll, the second write overwrites the first and some pods will miss one invalidation entirely. This needs a queue/stream/per-event key strategy instead of replacing one shared value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/Storage/InMem.hs` around lines 108 - 115, Both
refresh paths currently overwrite the single Redis key
"inmem:force:cleanup:timeofday" (setExp) causing missed invalidations; change
the design to append events instead of replacing them: update the writers (the
code that builds ForceCleanupExpiryValue and calls runInMultiCloudRedisWrite
setExp "inmem:force:cleanup:timeofday") to push the payload into a Redis append
structure (e.g., LPUSH to "inmem:force:cleanup:events" or XADD to a stream)
including forceCleanupTimestamp and forceCleanupKeyPrefix/keyInfix, and update
inMemCleanupThread to consume all queued events (BRPOP/LPOP loop or
XREAD/XREADGROUP) and process each payload rather than reading one shared key;
ensure the event list/stream has retention (trim/expire) so old entries are
removed.

Comment on lines 78 to +92
oldInfo <- liftIO $ readIORef (inMemHashMap inMemEnv)
let oldCount = HM.size (cache oldInfo)
case req.keyInfix of
(deletedCount, remainingCount) <- case req.keyInfix of
Nothing -> do
liftIO $
writeIORef (inMemHashMap inMemEnv) $
InMemCacheInfo {cache = HM.empty, cacheSize = 0, createdAt = now}
pure InMemRefreshResponse {deletedKeys = oldCount, remainingKeys = 0}
pure (oldCount, 0)
Just infix_ -> do
let toKeep = HM.filterWithKey (\k _ -> not (infix_ `T.isInfixOf` k)) (cache oldInfo)
newSize = foldl' (\acc info -> acc + info.cacheDataSize) 0 (HM.elems toKeep)
deletedCount = HM.size (cache oldInfo) - HM.size toKeep
newSize = foldl' (\acc keyInfo -> acc + keyInfo.cacheDataSize) 0 (HM.elems toKeep)
liftIO $
writeIORef (inMemHashMap inMemEnv) $
InMemCacheInfo {cache = toKeep, cacheSize = newSize, createdAt = oldInfo.createdAt}
pure
InMemRefreshResponse
{ deletedKeys = deletedCount,
remainingKeys = HM.size toKeep
pure (HM.size (cache oldInfo) - HM.size toKeep, HM.size toKeep)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the local refresh update atomic.

This snapshots the cache with readIORef and then overwrites it with writeIORef. A concurrent withInMemCache insert between those calls is lost, even when that key does not match req.keyInfix. refreshInMem already uses atomicModifyIORef; this path needs the same pattern.

Suggested fix
-  oldInfo <- liftIO $ readIORef (inMemHashMap inMemEnv)
-  let oldCount = HM.size (cache oldInfo)
-  (deletedCount, remainingCount) <- case req.keyInfix of
-    Nothing -> do
-      liftIO $
-        writeIORef (inMemHashMap inMemEnv) $
-          InMemCacheInfo {cache = HM.empty, cacheSize = 0, createdAt = now}
-      pure (oldCount, 0)
-    Just infix_ -> do
-      let toKeep = HM.filterWithKey (\k _ -> not (infix_ `T.isInfixOf` k)) (cache oldInfo)
-          newSize = foldl' (\acc keyInfo -> acc + keyInfo.cacheDataSize) 0 (HM.elems toKeep)
-      liftIO $
-        writeIORef (inMemHashMap inMemEnv) $
-          InMemCacheInfo {cache = toKeep, cacheSize = newSize, createdAt = oldInfo.createdAt}
-      pure (HM.size (cache oldInfo) - HM.size toKeep, HM.size toKeep)
+  (deletedCount, remainingCount) <- liftIO $
+    atomicModifyIORef (inMemHashMap inMemEnv) $ \oldInfo ->
+      case req.keyInfix of
+        Nothing ->
+          let oldCount = HM.size (cache oldInfo)
+              newInfo = InMemCacheInfo {cache = HM.empty, cacheSize = 0, createdAt = now}
+           in (newInfo, (oldCount, 0))
+        Just infix_ ->
+          let toKeep = HM.filterWithKey (\k _ -> not (infix_ `T.isInfixOf` k)) (cache oldInfo)
+              newSize = foldl' (\acc keyInfo -> acc + keyInfo.cacheDataSize) 0 (HM.elems toKeep)
+              deletedCount = HM.size (cache oldInfo) - HM.size toKeep
+              newInfo = InMemCacheInfo {cache = toKeep, cacheSize = newSize, createdAt = oldInfo.createdAt}
+           in (newInfo, (deletedCount, HM.size toKeep))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
oldInfo <- liftIO $ readIORef (inMemHashMap inMemEnv)
let oldCount = HM.size (cache oldInfo)
case req.keyInfix of
(deletedCount, remainingCount) <- case req.keyInfix of
Nothing -> do
liftIO $
writeIORef (inMemHashMap inMemEnv) $
InMemCacheInfo {cache = HM.empty, cacheSize = 0, createdAt = now}
pure InMemRefreshResponse {deletedKeys = oldCount, remainingKeys = 0}
pure (oldCount, 0)
Just infix_ -> do
let toKeep = HM.filterWithKey (\k _ -> not (infix_ `T.isInfixOf` k)) (cache oldInfo)
newSize = foldl' (\acc info -> acc + info.cacheDataSize) 0 (HM.elems toKeep)
deletedCount = HM.size (cache oldInfo) - HM.size toKeep
newSize = foldl' (\acc keyInfo -> acc + keyInfo.cacheDataSize) 0 (HM.elems toKeep)
liftIO $
writeIORef (inMemHashMap inMemEnv) $
InMemCacheInfo {cache = toKeep, cacheSize = newSize, createdAt = oldInfo.createdAt}
pure
InMemRefreshResponse
{ deletedKeys = deletedCount,
remainingKeys = HM.size toKeep
pure (HM.size (cache oldInfo) - HM.size toKeep, HM.size toKeep)
(deletedCount, remainingCount) <- liftIO $
atomicModifyIORef (inMemHashMap inMemEnv) $ \oldInfo ->
case req.keyInfix of
Nothing ->
let oldCount = HM.size (cache oldInfo)
newInfo = InMemCacheInfo {cache = HM.empty, cacheSize = 0, createdAt = now}
in (newInfo, (oldCount, 0))
Just infix_ ->
let toKeep = HM.filterWithKey (\k _ -> not (infix_ `T.isInfixOf` k)) (cache oldInfo)
newSize = foldl' (\acc keyInfo -> acc + keyInfo.cacheDataSize) 0 (HM.elems toKeep)
deletedCount = HM.size (cache oldInfo) - HM.size toKeep
newInfo = InMemCacheInfo {cache = toKeep, cacheSize = newSize, createdAt = oldInfo.createdAt}
in (newInfo, (deletedCount, HM.size toKeep))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/Storage/InMem/Management/Handler.hs` around
lines 78 - 92, The refresh path currently does a non-atomic readIORef then
writeIORef on inMemHashMap (inside refreshInMem handler using req.keyInfix),
which can lose concurrent inserts; change the logic to use atomicModifyIORef so
the snapshot and update happen atomically: perform the filter/clear and
recompute cacheSize inside an atomicModifyIORef on (inMemHashMap inMemEnv),
returning the new InMemCacheInfo and the deleted/remaining counts; reference the
existing InMemCacheInfo type, the inMemHashMap IORef, req.keyInfix, and keep
consistency with how refreshInMem already uses atomicModifyIORef and how
withInMemCache performs concurrent inserts.

Comment on lines +70 to +85
data PodCleanupAck = PodCleanupAck
{ podName :: Text,
success :: Bool,
errorMessage :: Maybe Text
}
deriving (Generic, Show, ToSchema)

podCleanupAckJsonOptions :: Ae.Options
podCleanupAckJsonOptions = Ae.defaultOptions {Ae.fieldLabelModifier = \f -> if f == "errorMessage" then "error" else f}

instance ToJSON PodCleanupAck where
toJSON = Ae.genericToJSON podCleanupAckJsonOptions

instance FromJSON PodCleanupAck where
parseJSON = Ae.genericParseJSON podCleanupAckJsonOptions

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="lib/mobility-core/src/Kernel/Storage/InMem/Management/Types.hs"

# 1) Show pragmas + imports around the file top
echo "===== Top of $FILE (pragmas/imports) ====="
sed -n '1,140p' "$FILE" | nl -ba

# 2) Show the full definition area around lines 60-110
echo
echo "===== Around PodCleanupAck (lines 50-120) ====="
sed -n '50,130p' "$FILE" | nl -ba

# 3) Find SidecarRefreshResponse definition nearby or where PodCleanupAck is used
echo
echo "===== Find SidecarRefreshResponse references in file ====="
rg -n "SidecarRefreshResponse|PodCleanupAck" "$FILE"

# 4) Show SidecarRefreshResponse definition (small region around any match)
for m in $(rg -n "SidecarRefreshResponse|PodCleanupAck" "$FILE" | cut -d: -f1 | sort -n | uniq); do
  start=$((m-25)); end=$((m+25));
  echo
  echo "===== Context around line $m ====="
  sed -n "${start},${end}p" "$FILE" | nl -ba
done | head -n 3000

Repository: nammayatri/shared-kernel

Length of output: 229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for schema/openapi generation / occurrences
echo "===== Repo search: PodCleanupAck ====="
rg -n "PodCleanupAck" -S .

echo
echo "===== Repo search: errorMessage within Mobility core ====="
rg -n "errorMessage" lib/mobility-core -S

echo
echo "===== Repo search: Ae.fieldLabelModifier usage ====="
rg -n "fieldLabelModifier" -S .

echo
echo "===== Repo search: genericToSchema / ToSchema options ====="
rg -n "generic.*ToSchema|ToSchema.*Options|json.*schema.*Options|genericToSchema" -S lib/mobility-core

echo
echo "===== Repo search: openapi/Swagger snapshots ====="
rg -n "(swagger|openapi|OpenAPI|Swagger|spec\\.json|swagger\\.json|openapi\\.json)" -S lib/mobility-core

Repository: nammayatri/shared-kernel

Length of output: 19967


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="lib/mobility-core/src/Kernel/Storage/InMem/Management/Types.hs"
echo "===== Top of $FILE (pragmas/imports) ====="
sed -n '1,160p' "$FILE" | nl -ba

echo
echo "===== Around PodCleanupAck (lines 60-110) ====="
sed -n '60,115p' "$FILE" | nl -ba

echo
echo "===== Find SidecarRefreshResponse definition lines ====="
rg -n "data SidecarRefreshResponse|SidecarRefreshResponse" "$FILE"

Repository: nammayatri/shared-kernel

Length of output: 229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="lib/mobility-core/src/Kernel/Storage/InMem/Management/Types.hs"

echo "===== Top of $FILE (pragmas/imports) ====="
sed -n '1,200p' "$FILE" | nl -ba

echo
echo "===== Around PodCleanupAck (lines 60-110) ====="
sed -n '55,120p' "$FILE" | nl -ba

echo
echo "===== Lines containing SidecarRefreshResponse / PodCleanupAck in file ====="
rg -n "SidecarRefreshResponse|PodCleanupAck" "$FILE"

echo
echo "===== Context around SidecarRefreshResponse occurrences ====="
while IFS=: read -r line rest; do
  start=$((line-25)); end=$((line+35))
  echo
  echo "=== ${FILE}:${line} ==="
  sed -n "${start},${end}p" "$FILE" | nl -ba
done < <(rg -n "SidecarRefreshResponse" "$FILE")

echo
echo "===== Repo search: PodCleanupAck ====="
rg -n "PodCleanupAck" -S .

echo
echo "===== Repo search: errorMessage ====="
rg -n "errorMessage" -S .

echo
echo "===== Repo search: fieldLabelModifier ====="
rg -n "fieldLabelModifier" -S .

Repository: nammayatri/shared-kernel

Length of output: 229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect local helpers for ToSchema naming conventions
echo "===== Search for ToSchema jsonOptions usage in mobility-core ====="
rg -n "genericToJSON|genericParseJSON|genericToSchema|genericDeclare|defaultOptions|Ae\\.defaultOptions" -S lib/mobility-core

echo
echo "===== Search for 'fieldLabelModifier =\\' in mobility-core ====="
rg -n "fieldLabelModifier\s*=\s*\\\\" -S lib/mobility-core

echo
echo "===== Search for ToSchema derived with custom json options elsewhere ====="
rg -n "defaultOptions.*fieldLabelModifier" -S lib/mobility-core

Repository: nammayatri/shared-kernel

Length of output: 50380


🌐 Web query:

servant-openapi3 ToSchema deriving Generic record field names respects aeson fieldLabelModifier

💡 Result:

Yes, the Generic implementation for ToSchema in the servant-openapi3 (via the openapi3 package) library supports customizing record field names using a mechanism similar to Aeson, but it requires explicit configuration of SchemaOptions [1][2]. While Aeson's generic derivation uses its own Options type, openapi3 provides a corresponding SchemaOptions type [1][2]. You can manually specify a fieldLabelModifier in SchemaOptions to transform record field names during schema generation [1][2]. To use custom field label modification: 1. Define your own SchemaOptions instance with the desired fieldLabelModifier [1][2]. 2. Use genericDeclareNamedSchema or genericDeclareSchema instead of the default ToSchema instance, passing your custom SchemaOptions [3][4]. Alternatively, the library provides a helper function, fromAesonOptions, which can convert Aeson Options into SchemaOptions [1][2]. This allows you to reuse the same logic you used for your Aeson ToJSON/FromJSON instances [1][2]. Note that while fromAesonOptions copies the fieldLabelModifier, it only affects the fields that map directly to the SchemaOptions type; other Aeson-specific options may be ignored by the schema generator [1][2]. If you are using the default deriving clause (deriving ToSchema), it uses the defaultSchemaOptions (which has fieldLabelModifier = id), meaning it will not automatically pick up your Aeson configuration unless you manually define the ToSchema instance to use your customized SchemaOptions [1][5][4].

Citations:


🌐 Web query:

servant-openapi3 genericToSchema aeson Options fieldLabelModifier

💡 Result:

To customize field names when generating schemas with servant-openapi3 (and the underlying openapi3 library), you use the SchemaOptions type [1][2]. If you already have Aeson Options defined for your generic JSON instances, you can convert them directly to SchemaOptions using the fromAesonOptions function [1][3]. This function automatically maps the fieldLabelModifier, constructorTagModifier, allNullaryToStringTag, and unwrapUnaryRecords fields from your Aeson Options to the corresponding SchemaOptions [1][3]. For manual configuration, define a custom SchemaOptions record where fieldLabelModifier is a function of type String -> String [1][4]. Example implementation: import Data.OpenApi import GHC.Generics data User = User { userName:: String, userEmail:: String } deriving (Generic) -- Define custom options mySchemaOptions:: SchemaOptions mySchemaOptions = defaultSchemaOptions { fieldLabelModifier = drop 4 -- Example: removes "user" prefix } instance ToSchema User where declareNamedSchema proxy = genericDeclareNamedSchema mySchemaOptions proxy When syncing with existing Aeson generic instances (e.g., genericToJSON), ensure your SchemaOptions match the logic in your Aeson Options to guarantee the generated OpenAPI specification accurately reflects your API's JSON output [5][6]. Note that while fromAesonOptions copies essential fields, some Aeson options like omitNothingFields have no effect on SchemaOptions and must be handled separately if needed [1][3].

Citations:


🌐 Web query:

deriveToSchema fieldLabelModifier aeson Options

💡 Result:

In the Haskell aeson library, fieldLabelModifier is a field within the Options data type used to customize how Haskell record field names are mapped to JSON keys [1][2]. The fieldLabelModifier function has the signature (String -> String) and is applied to each record field name during instance derivation [3][2]. When using Template Haskell for derivation via functions like deriveJSON, you modify the defaultOptions record to include your custom modifier [1][4]: $(deriveJSON defaultOptions { fieldLabelModifier = drop 4 } ''MyDataType) If you are using GHC.Generics-based derivation (via genericToJSON, genericParseJSON, etc.), you similarly apply the modifier to the Options object [2][5]: customOptions:: Options customOptions = defaultOptions { fieldLabelModifier = drop 1 } instance ToJSON MyDataType where toJSON = genericToJSON customOptions Common patterns for fieldLabelModifier include: - Removing prefixes: Using functions like drop or custom string manipulation to strip leading underscores or type prefixes from field names [1][2][4]. - Converting case: Using helper functions like camelTo2 from Data.Aeson.Types to convert camelCase Haskell fields to snake_case JSON keys [2]. - Mapping specific names: Using a lambda or named function to rename specific fields (e.g., to avoid reserved keywords or match specific API requirements) [3]. Always ensure that you use the same Options value for both ToJSON and FromJSON instances to maintain consistency between encoding and decoding [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Use sed/cat since `nl` isn't available in the sandbox.
FILE="lib/mobility-core/src/Kernel/Storage/InMem/Management/Types.hs"

echo "===== PodCleanupAck + SidecarRefreshResponse (around lines 60-110) ====="
sed -n '60,110p' "$FILE" | cat -n

echo
echo "===== Repo search: fromAesonOptions usage ====="
rg -n "fromAesonOptions" -S lib/mobility-core || true

echo
echo "===== Inspect Kernel/Utils/Schema.hs for schema options helpers ====="
SCHEMA_FILE="lib/mobility-core/src/Kernel/Utils/Schema.hs"
sed -n '1,120p' "$SCHEMA_FILE" | cat -n
sed -n '120,240p' "$SCHEMA_FILE" | cat -n

Repository: nammayatri/shared-kernel

Length of output: 5640


Sync OpenAPI schema field name with Aeson JSON rename for PodCleanupAck

podCleanupAckJsonOptions renames JSON key errorMessageerror, but PodCleanupAck only deriving (ToSchema), so the generated schema will still document the property as errorMessage (and this propagates through SidecarRefreshResponse.pods). Add an explicit ToSchema PodCleanupAck using matching SchemaOptions (e.g., fromAesonOptions podCleanupAckJsonOptions / equivalent) or stop renaming in JSON.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mobility-core/src/Kernel/Storage/InMem/Management/Types.hs` around lines
70 - 85, The OpenAPI schema for PodCleanupAck is out-of-sync with the Aeson JSON
rename: podCleanupAckJsonOptions maps errorMessage → "error" for ToJSON/FromJSON
but PodCleanupAck still derives ToSchema (so the schema will expose
"errorMessage"); update this by providing an explicit ToSchema PodCleanupAck
that uses the same Aeson options (e.g., use
genericDeclareUnNamedSchema/fromAesonOptions with podCleanupAckJsonOptions) so
the generated schema matches the runtime JSON keys (affecting PodCleanupAck and
SidecarRefreshResponse.pods), or alternatively remove the rename in
podCleanupAckJsonOptions so schema and JSON already align.

@piyushKumar-1 piyushKumar-1 merged commit bfbf24d into main Jun 6, 2026
2 checks passed
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.

1 participant