Invalidate the Price After a Committed Write

RedisBeginner
Practice Now

Introduction

A staff price change should update the authoritative database and remove the old cached value for that exact price. Clearing every cached product would discard useful work. Removing a cache entry before a failed source write would also misrepresent the write's outcome.

This independent lab prepares a new cache desk and a correct full-identity, 15-second read policy. You need the preceding lab's cache-aside, identity, TTL and short Python-template concepts. You will complete change_price, observe a rejected primary write, and compare explicit invalidation with an external update that becomes visible after natural expiry. Only Terminal is needed; no files or processes carry over from the previous VM.

Edit only /home/labex/project/cache-desk/policy.py, using the three documented functions and source/cache API. Keep all tools, source table identities, unrelated prices and the catalog note. The permitted primary changes are North MUG USD 1200 to 1350 through your policy, a rejected negative-price attempt, and the prepared external change to 1450. Requests run sequentially with healthy services in the learner path; this is not strong consistency, distributed cache invalidation or a promise that two databases commit atomically. Estimated time is the author's reading-and-error allowance, without beginner timing data.

Invalidate the Exact Price After Commit

In this step, you will make a successful source write remove its stale cached price, while preserving the cache after a rejected write.

Start in Terminal. Enter this independent unit's prepared workspace:

cd /home/labex/project/cache-desk
./desk status
./desk policy

The three identity fields are shop, SKU and currency. North MUG USD begins at 1200 cents. South MUG USD is 1500, North MUG EUR is 1100, and North FREE USD is 0. Warm a normal price, then submit a legitimate change:

./desk view north MUG USD
./desk view north MUG USD
./desk change north MUG USD 1350
./desk status
./desk view north MUG USD

Status now shows the committed primary price 1350, while a prompt view can still return cached 1200. If its 15 seconds have already elapsed, the view may correctly miss and show 1350; the missing invalidation remains a policy defect even when expiry hides it. The success message only says the submitted policy returned; actual primary and cache observations decide what happened.

change_price already calls source.write(identity, cents). This prepared method updates one known primary row in a native SQLite transaction. It returns only after commit. A negative price violates the source's CHECK constraint and raises an error; an unknown identity raises a lookup error. Python runs the next line only if the preceding call returns successfully. Therefore place the exact Redis deletion after this call. cache_key(prefix, identity) reuses the same identity mapping as the read function. Do not delete by SKU alone or use FLUSHDB/FLUSHALL.

As in the previous lab, the quoted here-document replaces only policy.py; preserve indentation. The full read function is repeated so the replacement is complete:

cat > policy.py <<'POLICY'
def cache_key(prefix, identity):
    return f"{prefix}:{identity[0]}:{identity[1]}:{identity[2]}"


def read_price(source, cache, prefix, identity):
    key = cache_key(prefix, identity)
    value = cache.command("GET", key)
    if value is not None:
        return int(value), "cache"
    cents = source.read(identity)
    cache.command("SET", key, cents, "EX", 15)
    return cents, "source"


def change_price(source, cache, prefix, identity, cents):
    source.write(identity, cents)
    cache.command("DEL", cache_key(prefix, identity))
POLICY

Warm two unrelated prices, then repeat the allowed write. Writing the same 1350 still performs a real known-row commit and exercises invalidation:

./desk view south MUG USD
./desk view north MUG EUR
./desk change north MUG USD 1350
./desk inspect north MUG USD
./desk view north MUG USD
./desk view north MUG USD

Inspect must show the target key missing immediately after the write. The next view returns source/1 and 1350; its next view is cache/0. Other keys are not deliberately invalidated; they can still expire naturally. Use status to distinguish preserved primary values from cache presence.

Now deliberately request an invalid negative price. The following command is expected to exit nonzero and print IntegrityError with the native CHECK failure. It is not a successful update, a cache miss or a connection problem:

./desk change north MUG USD -1
./desk status
./desk inspect north MUG USD
./desk view north MUG USD

The authoritative price remains 1350. If the warmed entry has not naturally expired, its value and original deadline remain, and the view is cache/0. A slow reader may instead see a normal expiry; the isolated check tests failed-write preservation without a reading-time race. Python stops before DEL when source.write raises. Do not catch the error and print a false success.

If the shell reports ./deskk missing, run ls and correct the name. If your policy has a syntax or indentation error, use ./desk policy to inspect it, then replace it with the full block again. A failed negative-price command needs no reset: the prepared source transaction has already rolled back that attempt. Follow it with a read, not a direct database edit.

Let an External Update Reach the Next Fill

In this step, you will observe a real external source change and the bounded lifetime of an old cached value.

Continue in Terminal, in /home/labex/project/cache-desk. Reapply the allowed committed price to remove any existing target cache, then fill 1350. This makes the starting point clear without changing another product:

./desk change north MUG USD 1350
./desk view north MUG USD
./desk inspect north MUG USD

The external command represents a deliberately controlled writer that bypasses this application's invalidation hook. It performs a real primary commit, not a timer-driven display change. This unit permits only the following external target and new price:

./desk external north MUG USD 1450
./desk status
./desk view north MUG USD
./desk inspect north MUG USD

Status shows 1450 immediately. A view before the existing key expires can still return cached 1350 with zero primary reads. Its deadline continues to count down; it does not restart on the hit. If time spent reading already exceeded the lifetime, seeing 1450 from a miss is expected. You can repeat this step's initial change/fill/external sequence promptly to observe the stale interval.

Wait past the complete configured fill lifetime before the final read:

sleep 16
./desk inspect north MUG USD
./desk view north MUG USD
./desk view north MUG USD
./desk status

Inspect shows the missing key. The next view reads committed 1450 from the primary and fills Redis; the following view can use that actual cached 1450. Keep this final primary state. All other authoritative prices remain 1500, 1100 and 0. ./redis GET catalog:admin:note can inspect the preserved unrelated note, without making a primary request.

This is a sequential cache-aside policy. An external update after a fill can leave that fill's old value visible until its existing 15-second deadline. That observation does not prove a global staleness bound under concurrent reads, writes, slow fills or a stopped service. A concurrent reader could fetch an old source value before a write and fill it after invalidation; this course does not solve that race or a stampede of simultaneous misses.

The source commit and Redis deletion are separate operations. If Redis refuses the deletion after a successful commit, the primary is already changed. The error must propagate as a cache-operation failure; it must not be described as a primary rollback or a completed invalidation. The isolated grader tests that native failure without breaking your live service. Production retries, durable invalidation and outage recovery need additional design, outside this exercise.

Summary

You linked a real primary commit to exact cache invalidation, preserved cached data after a rejected source write, and observed an external change through natural expiry. Your policy distinguishes a source-write failure from a later Redis failure and makes its sequential consistency boundary explicit.