Give Cached Prices a Clear Identity and Lifetime

RedisBeginner
Practice Now

Introduction

A small shop keeps authoritative prices in a prepared primary database and uses Redis to remember prices customers have already viewed. Repeated views should use that remembered value. But one SKU can have different prices in different shops or currencies, and a remembered price must eventually expire.

You will deliver a short policy.py that chooses the full cache identity and gives every fill a finite lifetime. You need basic terminal navigation and Redis GET/SET knowledge. The Python template and every expression you change are explained here; no previous Python course is required. The primary is a prepared SQLite database, not a second database implementation task. This independent unit uses only Terminal, with its own fresh data.

Edit only /home/labex/project/cache-desk/policy.py. Keep the supplied tools, source table, all source identities and prices, and the unrelated catalog note. Use only the documented source/cache API and ordinary policy expressions in the three functions; keep imports, shell execution and unrelated resources out of the policy. Do not disable caching, clear Redis broadly or change global configuration. We test controlled sequential requests, without a production latency or strong-consistency guarantee. The author's estimated time includes reading and ordinary mistakes; it has not been timed with beginners.

Separate the Prices Customers Actually Request

In this step, you will observe a real cache hit, find a key collision, and make every price's full identity part of its key.

Start in Terminal, at the shell prompt. cd changes the current directory; this absolute path works from any directory. ./desk runs a prepared program from that directory. Each command below is a separate shell command, not input for an interactive Redis prompt.

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

Read the column order: shop / sku / currency / integer cents. North MUG USD costs 1200 cents, South MUG USD costs 1500, North MUG EUR costs 1100, and North FREE USD costs 0. The status display reads authoritative prices; it never fills the cache.

A cache miss means Redis has no value for the requested key. A cache hit means it does. This prepared application uses cache-aside: read Redis first, read the primary only on a miss, then remember the result. Try the same normal request twice:

./desk view north MUG USD
./desk view north MUG USD

The first result should show "cents": 1200, "origin": "source", "primary_selects": 1; the second shows "origin": "cache", "primary_selects": 0. The count comes from native SQLite SELECT traces for this request, not an estimated speedup. JSON output uses quoted field names; these three fields identify price, origin and primary reads.

Now request another shop and inspect the actual Redis key:

./desk view south MUG USD
./desk inspect south MUG USD
./desk policy

The incomplete key uses only MUG. It can return North's 1200 for South, even though the primary still stores 1500. inspect reads the key and its actual value; it does not repair the price. The supplied prefix catalog scopes this course's keys. Every key must start with the supplied prefix followed by a colon, such as catalog:. The suffix must distinguish all three identity fields, not just the SKU; equivalent suffix encodings are accepted.

The template is Python. def defines a function; the names inside parentheses are its arguments. An indented body belongs to that function. return supplies its result. identity is a three-item tuple, in the displayed field order: identity[0] is shop, [1] is SKU, [2] is currency. Indexing starts at zero. f"...{name}..." inserts an expression into a string; colons here are ordinary separators. Course identifiers contain only letters, digits, hyphens and underscores, so they cannot contain that separator. This key format is not a general encoding for arbitrary user text.

In read_price, = assigns a value. cache.command("GET", key) sends a real Redis command. None means no cached value; if value is not None: branches only when a value exists. This explicit test preserves a zero price. Redis returns its string value, so int(value) converts it to integer cents. The comma in return cents, "source" returns a pair. source.read(identity) issues the prepared primary SELECT and raises a lookup error for an unknown identity. There is no fallback reference price.

Save a complete file with a full key. cat > policy.py replaces that file; the quoted here-document marker 'POLICY' makes the shell copy every following line literally until the standalone POLICY line. Copy the whole block, keeping four spaces of indentation. The repeated functions retain the prepared read behavior; only the key decision changes in this step.

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)
    return cents, "source"


def change_price(source, cache, prefix, identity, cents):
    source.write(identity, cents)
POLICY

View each identity twice:

./desk view north MUG USD
./desk view north MUG USD
./desk view south MUG USD
./desk view south MUG USD
./desk view north MUG EUR
./desk view north MUG EUR
./desk view north FREE USD
./desk view north FREE USD

Each first read of a new full key comes from the source, and its second from Redis. Prices are 1200, 1500, 1100 and 0 respectively. A cache hit with a wrong price is not success. The old disposable catalog:MUG key is no longer used; no broad deletion is needed.

If you type ./deskk, the shell reports that the file is missing. ls lists the actual tools; rerun ./desk with the right spelling. If Python reports a syntax or indentation error, display ./desk policy, then copy the complete file block again with its leading spaces intact. Do not reset the VM or change the prepared tools. This step's check exercises changed identities in a disposable fixture and preserves the live primary; it does not require a particular key spelling.

Give Each Fill a Deadline

In this step, you will add a finite lifetime when a price enters Redis, and keep cache hits from moving that deadline.

Continue in Terminal, in /home/labex/project/cache-desk. Inspect an existing full key:

./desk inspect north MUG USD

pttl_ms is remaining lifetime in milliseconds. -1 means the key has no expiry; -2 means it is absent. A positive value counts down. An unexpired value without a deadline can survive an external price change indefinitely.

Redis SET key value EX seconds stores the value and its expiration in one command. Choose 15 seconds for this teaching shop; accepted fills must start between 12 and 20 seconds. This short interval makes natural expiry observable; it is not a production TTL recommendation. Set the deadline only on a miss. Renewing it on every hit can keep a popular stale price alive indefinitely. A plain SET that overwrites an expiring key clears the old expiry unless preservation was explicitly requested.

Replace the file with the same full identity and a timed SET. In Python, the separate arguments "EX", 15 are passed to Redis; 15 is an integer, not a quoted identifier.

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)
POLICY

The full-identity values filled in the previous step still have no expiry: changing code does not retroactively add a deadline. forget deliberately deletes only one known identity's current cache key, leaving its primary price and other keys alone. Remove these four explicitly disposable practice entries individually. This is an explicit learner action, never a broad flush or hidden reader repair. Subsequent fills will all use the new deadline. Then fill North's price and inspect it:

./desk forget north MUG USD
./desk forget south MUG USD
./desk forget north MUG EUR
./desk forget north FREE USD
./desk view north MUG USD
./desk inspect north MUG USD
./desk view north MUG USD
./desk inspect north MUG USD

The first view is source/1, the next is cache/0 if issued before expiry. Positive PTTL decreases instead of restarting at 15000. If reading takes longer than 15 seconds, an actual miss is expected: repeat the fill and inspect commands promptly, without changing your policy.

You can also read native Redis values through the supplied read-only connection. ./redis sends its arguments to this course's private Redis, not the generic Redis service:

./redis GET catalog:north:MUG:USD
./redis TTL catalog:north:MUG:USD

The wrapper prints a JSON string such as "1200", a native integer TTL, or null for a missing value. It never fetches a missing price. Now sleep 16 asks the shell to wait 16 real seconds. It does not simulate time or delete a key for you:

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

Before refilling, inspect shows value: null and PTTL -2. The next view reads the source again, and the next can hit Redis. All authoritative prices stay unchanged in this unit. The grader tests actual expiry on isolated keys; live keys may naturally expire while you read, so finishing does not depend on racing a short countdown.

Summary

You separated shop, SKU and currency in real Redis keys, preserved zero prices, verified cache hits against primary reads, and gave each fill a non-sliding lifetime. Your policy is reusable; the prepared primary and unrelated catalog data remain intact.