Repository navigation
Conversation
The section says when a middleware prepares, what it is called with, where its answer goes, how a refusal is answered, and what each phase's preparation must not depend on. The changelog records the addition.
`PlatformLoadingCache` in `platform/loading_cache.rs` takes a key and a loader and returns the value, running the loader at most once per key at a time, so however many requests ask for a missing value at the same moment the source is asked once and every one of them continues with its value or its error. A failed load is not stored, so the next caller loads again. `NoLoadingCache` holds nothing and runs every load, and is what `RuntimeServices` carries when no module supplies a cache. Every key is a `StoreKey`, being the publisher's `ServerKeyPrefix`, the cache's name and the caller's key. The prefix is made in one place, `platform/server_key.rs`, from the publisher's domain in the loaded settings with a single leading `www.` folded and `ts.` in front, so two publishers sharing a store never share an entry. `PlatformValueCache` is the host's shared cache of small values with lifetimes, which a host supplies through `RuntimeServices` when it has one, with `lookup_or_reserve` handing the caller a `ValueReservation` where the host can make other callers for a key wait, and `remove`. A loading cache keeps its shared copies there where the host has one and in the key-value store otherwise. An `IntegrationBuilder` offers a loading cache with `with_loading_cache`, and `loading_cache_from` gives an adapter the one its linked builders offer, refusing two, so no adapter names the crate that supplies it. Core's tests get `MemoryLoadingCache`, which keeps what it loads for the lifetime the load gave and records every call.
A contributor's answer is loaded through `RuntimeServices::load_cached`, so the contributor is asked once however many requests want its rules at the same moment, and the copy in the host's shared store answers every instance until the contributor's refresh is due. The answer's lifetime in the cache is the contributor's refresh, and `[cache.robots-txt]` sets how long the copies last with `memory_seconds`, `store_seconds` and `store_idle_seconds`, defaulting to a minute, a week and 30 days. A memory lifetime may be 0, the other two must be at least 1, and none may be more than a year, which `LoadingCacheConfig::check` refuses as the settings load, naming the setting. The last answer a contributor gave is also kept apart from the cache for 30 days, in the host's value cache where it has one and in the key-value store otherwise, and read only when the contributor fails, because yesterday's rules are a better answer than none. Every key starts with the publisher's `ServerKeyPrefix`, so two publishers sharing a store never share an answer. The handler hands the settings through in place of the publisher's domain and the time, and a selection carries the name the contributor was declared under. The tests use `MemoryLoadingCache` and cover the one load for a held answer, the key naming the publisher, changed settings asking again, the kept answer served when the contributor fails, in a second instance and from a value cache, and a failure with nothing kept being an error that is not held.
Each adapter takes the cache from the builders it is given with `loading_cache_from` when it builds its state, and hands it to every request's services, so every request in a process shares the cache's memory copy and waits for one load. Axum, Cloudflare and Spin take it from the stock list followed by the builders a deployment passes, as they take every vendor module, and hold nothing without one. No adapter names the crate that supplies it. On Fastly the finishing steps' services carry the same cache as the request, so a lookup there shares the request's memory copy, and the entry point builds those services once from the state it has, or with the unavailable store and a cache that holds nothing when no state was built. Fastly supplies `FastlyValueCache` over Fastly's cache as the value cache, which needs nothing created or linked on the service. A transaction lookup reserves a key nothing holds for this instance and makes every other instance at the location wait for it to be filled, a stale entry is reserved for a fresh load rather than served, and a reservation dropped unfilled is released so a waiting instance takes it over. Each entry is written with a surrogate key, a hash of its key, which `remove` purges. Its tests run under Viceroy and cover the reservation, its release, a stale entry and the purge.
`crates/loading-cache/51degrees` implements `PlatformLoadingCache` with the published `fiftyone-caching` package, version 4.5.8-49, with its default features off so it builds for every host. Each cache is a least recently used copy in process memory with a short lifetime over the host's shared store, being its value cache where it has one and its key-value store otherwise, written in the package's entry format so a store that deletes late or keeps no lifetimes never returns an entry past its drop time. The memory layer is shared by every request in a process and loads a missing key once, covering the store read and the source together. The store layer is made for each call from that request's stores. A load a cache allows to run apart from its caller runs on a pool of threads on a native host and with the request's `wait_until` on Cloudflare, and every load runs in its caller on Fastly and Spin. The crate is the module `loading-cache.51degrees`, named by its folder, and `builder()` offers the cache whatever the settings say, because a loading cache is how a deployment caches rather than a module a document selects. The stock list carries the builder, so every adapter takes the cache from it with `loading_cache_from` and names no crate, and a test holds the list to offering one cache that keeps what it loads. The crate carries the maintainers table, the workspace lists it, and the Axum aliases lint and test it, where its own tests run natively. The crate's tests ask for one key from 16 callers at once on 16 threads with the pool, on 16 threads with loads in the caller, and on one thread with each load a task of its own, and the source runs once each time. Other tests cover a failure reaching every waiter and not being kept, the store answering a second process, the brief memory copy, idle expiry, a value's own lifetime, a reserving value cache being filled and released, two publishers kept apart, removal, the module's name and the builder's offer.
Spin's key-value store refuses a write with a lifetime, so a robots.txt answer kept for a later failure, and a loading cache entry in the module's key-value layer, are written without one there, as the held answer was before. The entry format carries each entry's own drop time, so a copy in a store that keeps no lifetimes still ends when it should, and a kept answer stays until it is replaced, which is what the configuration guide says of Spin. The robots.txt load carries the name the contributor was declared under as the static text the module context takes, and the handler hands it over from the selection. Tests cover the kept answer's lifetime where the store takes one, the answer kept where it refuses one, and a store that refuses lifetimes still answering a second process through the module.
The configuration guide gains "Loading caches" under the cache section, saying what is held, where the two copies live, how the keys start, which crate a stock build caches with and the `[cache.robots-txt]` lifetimes, and its robots.txt section says each answer is held through it. The integration guide gains "Supplying a loading cache", being how a crate offers one with `with_loading_cache` and how code loads through one with `load_cached`, a `CacheSpec`, a `StoreKey` and a `Loader`, and the builder table lists the offer. The example configuration shows `[cache.robots-txt]`, and the changelog records the cache, the lifetimes, the key prefix and the value cache under Unreleased.
The crate keeps a load running with the request's `wait_until` only where the Worker's handling of the request is wrapped in `scope_request`, and nothing on an adapter wraps it, so the crate's documentation and the changelog say so rather than that the load keeps running after the response on Cloudflare.
The item exists only on the Cloudflare target, so a link to it from the crate's documentation is reported broken on every other target's documentation build. The name is written as plain code instead.
The first build adds the published fiftyone-caching crate, hashlink and tokio-util to the lock, which a build with --locked refuses until they are committed.
A feature that holds answers in a loading cache names, in its own `[cache.<name>]` table, the module whose cache holds them, as the module's short name or its name in full. `loading_caches_from` resolves every feature's selection against the caches the linked builders offer, makes each module's cache once however many features select it, and refuses a name no builder offers a cache from, naming the feature and the modules that do. The services carry the selections as `LoadingCaches`, and a feature that selected none loads through `NoLoadingCache`. Before this the stock build's one offered cache held every feature's copies, and a deployment linking two caches was refused. Now each feature decides, so the asset cache and any later feature can choose a cache of its own. `[cache.robots-txt] module = "51degrees"` selects the stock cache for the robots.txt modules' answers. The guides, the example configuration and the changelog say so, and the adapters pass the selections where they passed the one cache.
Core's own middleware `robots_txt`, in `robots_txt/middleware.rs`, makes the file `[robots-txt]` describes where a `[[fetch]]` or `[[serve]]` entry for `text/plain` on `/robots.txt` names it. Its preparation does what the route's handler did, a refusal of every crawler from nothing and otherwise every selected contributor's held answer assembled, and answers a refusal with `503` and `Retry-After` when a contributor fails with nothing held, so nothing is stored and no empty file is served. Its action replaces whatever the origin sent with the file, and the creating rule makes the file where the origin has none. The registry makes the middleware itself once every registration is in, because the contributors it asks are known only then and a middleware cannot reach the registry. The settings refuse a `[robots-txt]` section that no entry places, because the file would never be made. The route goes from the four adapters, with `handle_robots_txt`, the response builder and the route's own cache lifetime. The file takes the deployment's cache policy for the path, as every file does. The adapters' route tests go with it, and each now proves the path is no route of this server; the file itself is proven through the publisher path in core. The stock list carries the middleware on `text/plain`, and the example configuration places it.
The configuration guide's robots.txt section says the file is made by the `robots_txt` middleware where an entry names it, in place of the origin's file or where the origin has none, that a section no entry places is refused, and that the response takes the deployment's cache policy for the path. Its example carries the entry, and the placing table has the middleware's row. The changelog records the middleware and the route's removal.
`[shared.<vendor>]` holds what every module of one vendor reads, such as an endpoint and a license key, so the key is written once. Core declares the tables and reads none of them. A vendor's crates read their own as typed settings through `Settings::shared_config`, which refuses a table that cannot be read as the type or fails its validation, naming the table. A table whose name is not a folder name is refused when the settings load. A builder marked with `with_shared_settings` is run by its vendor's table rather than by a selection, so a document without the table runs none of that vendor's modules, and a selection naming one of them is refused with the absent table named. A table that no builder reads is refused when the registry is built, the one place every builder is known. A secret in the table is declared with the builder's secret settings at its path inside the table and resolved when the table is present, as a module's own secrets are. The seam probe gains a second builder its table runs, whose route answers the table's marker, so the Axum adapter proves the table from outside core.
The configuration guide lists `[shared]` among the sections and describes the tables, what runs a builder, the refusals and how a secret in a table is written. The integration guide's builder table gains `.with_shared_settings(...)` and a section on reading the table as typed settings. The example configuration notes the table beside its list of sections, and the changelog records the addition.
Core gains a prefix module, `PrefixModule`, which gives two things for the publisher a request resolved to: a public prefix that starts every name the browser holds, being cookies and first-party storage keys, and a server key prefix that starts every key in a store publishers share. Core's own module gives `ts` and `ts.<publisher.domain>` and runs when `[prefix] module` selects nothing, so a stock build's names are what they were. The registry resolves the selection as it does geo and device, refuses a name nothing this deployment runs supplies, and asks the module once when the registry is built, so a module naming a value its call never carries is refused at startup. Every adapter puts the resolved prefixes on the request's services, and the Fastly entry point keeps what its request resolved to for the finishing steps that run after the request has been handed to the router. The robots.txt holding and the loading cache take the prefix from the request's services rather than from the settings.
The cookie constants lose their fixed `ts-` head and name the entry alone, `ec`, `ecr`, `ec-pull-complete`, `eids` and `tester`, and every read and write names the cookie through the request's public prefix, so a deployment whose module gives `_ex` reads and writes `_ex-ec`. The Edge Cookie state carries the prefix it was read under, and a module's response header naming a cookie under that prefix is refused as a cookie under `ts-` was. The page's bundle tag tells the client script the prefix, as `data-ts-public-prefix`, so the script names its own cookies after it and holds no copy. The template fingerprint includes the prefix, so a page stored under one is never served under another, and a key cookie naming one of the request's own identity cookies shares no template.
A prefix's first part starts with a letter or a single `_`, so no cookie name starts with `__`, where browsers give names such as `__Host-` rules of their own and drop a cookie that breaks them. The Edge Cookie test reads and writes under a prefix carrying a publisher's name as well, and a test holds the client script to the attribute the server writes.
The probe declares a prefix module on its registration, giving `_probe` and `probe.<publisher.domain>`, and its report route answers the prefixes the request's services carry, so the Axum adapter proves from outside core that `[prefix] module` resolves the module and that a request's services carry what it gives.
The configuration guide lists `[prefix]` among the sections and describes the selection and the names it changes. The integration guide's registration table gains `.with_prefix_module(name, ...)` and a section on giving the deployment its prefixes. The example configuration notes the section, and the changelog records the addition and the renamed cookie constants.
`DeviceSignals` gains `human_confidence`, one of `High`, `Medium`, `Low` and `Unknown`, which a device module that scores the request writes and every other module leaves `Unknown`. `Unknown` is kept apart from `Low` because a request alone cannot establish a person, so a first-time visitor is not called a machine. Nothing in core gates on the answer; `looks_like_browser` still gates the Edge Cookie as it did, and the answer is there for a module that reads it.
`DeviceAttributes` carries what a bidder is told about the device, being the OpenRTB device type, make, model, operating system and version, and screen size, every field optional because every source is partial. It is kept apart from `DeviceSignals`, which answer whether the request looks like a browser and are stored in the identity graph, because these answer what a bidder prices and are stored nowhere. `DeviceModule::advertising_attributes` answers them from the module call `detect` is handed, with a default of nothing so no existing module changes. `attach_device_attributes` asks the selected module after each auction request is built, on the auction endpoint and the publisher's server-side path and not the consent-denied path, which makes no outbound call. A module that resolves nothing adds no device object, because an absent object and an empty one say the same to a bidder and the absent one is smaller. The OpenRTB device object carries the attributes, each absent where nothing resolved it, and `device_type_from_name` maps a detection name to the OpenRTB code and maps nothing for a name it does not know, because a wrong type is worse for a bidder than an absent one.
The probe's device module scores the request `Medium` and answers fixed attributes, a tablet by name, so the Axum adapter proves from outside core that the module `[device] module` resolves answers both through the call it is handed, and that a tablet maps to OpenRTB device type 5.
The integration guide's table of what a module is handed gains the device module's second answer, the attributes for the bid request, and the configuration guide's device section says a module may score the request and tell a bidder what the device is. The changelog records the addition.
`CreativeOpportunityFormat` gains optional `min_viewport_width` and `max_viewport_width`, in CSS pixels. A slot with a bounded format carries `viewport_sizes` in its page data, one band per width at which the formats on offer change, widest first, and the Google ad tag's head bootstrap and the bundle's slot definition apply it as a size mapping when they define the slot, so a slot asks for a large size only where the page has room for one. A slot whose formats name no bound behaves exactly as before, and a server-side auction is still offered every format. A binary without the fields refuses a document carrying them, so it is promoted before any configuration uses them.
`MiddlewareAction` gains `body_open_inserts`, markup written once per document immediately after the opening `<body>` tag, in the order given, beside the places it had for the head and for straight after the script bundle. A vendor's two-part installation may put its second part there and nowhere else, as Tag Manager's noscript frame is, and until now a middleware had no way to reach that place. A document with no body gets none, and a second body tag in a template fragment gets nothing.
A publisher whose page templates forbid third-party scripts cannot put a Google tag on the page, so it has to come from the proxy, and nothing did that: the Tag Manager module re-serves a tag the page already carries, which needs a page that already loads Google. `crates/tag/google-tag`, selected by `[tag] modules` with `[tag.google-tag]`, writes Google's own snippets. At the start of head, the consent mode defaults, then Tag Manager's container script, then the gtag.js snippet, then the AdSense loader. Immediately after the opening body tag, Tag Manager's noscript iframe. Each is copied from Google's documentation rather than paraphrased and carries the page it came from, so a reader can compare the two. Consent defaults come first because Google's consent guide requires the default command before anything that sends measurement data, and all four types default to denied because no Google identifier may be stored before a permission signal has been processed. Where each loader points is settled at startup, because head and body insertions never reach the attribute rewriters. When the Tag Manager module serves the same tag or container, the loader names the first-party path, otherwise Google's own address, and the Tag Manager crate offers the two paths and the two id shapes it accepts so the two modules cannot disagree about an id. The noscript iframe always names Google, because `ns.html` has no first-party route behind it. A page also loading gtag.js for the same tag has that duplicate loader removed, which is why the module registers before Tag Manager in the stock list, since a removal short-circuits the rewriter chain. An inline configuration call in the publisher's own page is not removed and still sends a second page view, which the guide says plainly, because removing it would mean buffering every response.
The published pre-release the module was pinned at is superseded by the released 4.5.8, so the module reads `^4.5.8` and the lock file names it. Nothing else changes.
…once A route whose parameter the request's permissions withhold answers 403 as before. A value absent where the route runs, or needing permissions that are not resolved there, is a fault in how the module was written or placed, and answers 500 rather than 403. The injection rule is stated in the module_context rustdoc and the trait methods link to it. The route helpers' repeated doc sentence, three repeats of the Edge Cookie snapshot comment and the guide's copy of the parameter table are removed. A misplaced test helper doc is moved back to its function, and the device-signal gate's limit for a module the adapter builds is stated.
# Conflicts: # docs/guide/configuration.md
# Conflicts: # docs/guide/configuration.md
…as the workspace config does
12 of 14 tasks
A page that carries no GPT loader of its own got the bootstrap, the shim and the slot handoff from this module and nothing that loads GPT, so its slots never filled. `[ad-tag.google] write_loader = true` writes the loader tag into the head, last among the module's inserts, after the shim and the bootstrap it relies on. The tag asks for the loader on the module's own route, `/integrations/gpt/script`, so the whole chain stays first party. It is off by default, because a page that already carries the loader would otherwise load it twice. The guide's two tables and the GPT page name the setting, the example settings file shows it, and the changelog says what it adds. Three tests cover the tag written when asked, written last beside the slim-Prebid address, and absent by default.
rustfmt breaks the two assertions over several lines, and prettier pads the middleware table's row to the table's width.
jwrosewell
marked this pull request as ready for review
October 14, 2026 12:36
Collaborator
|
Moved into this repository as #1372, one branch of the stack, so the review continues there. This pull request is closed in its favor and its comments stay here for the record. |
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.
Stacks on the Google tag module (#1318). The additions that follow the seven pull requests open here are listed in #777.
Compare
split/30-google-tagwithsplit/31-caching-releaseto see only this pull request's change.
What this pull request does
fiftyone-cachingmoves from the pre-release4.5.8-49to the release4.5.8in the root manifest and in the lock. The crate's source is the same at both versions, so nothing else moves. The pin was placed by the loading cache module (#1312) when no release carried the crate.How it was verified
CI passed on
302094defon the fork.References #1282.