Skip to content

Commit 6983ba5

Browse files
committed
added AGENTS.md & DOCS
1 parent a46c50d commit 6983ba5

3 files changed

Lines changed: 194 additions & 0 deletions

File tree

‎.gitattributes‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
.gitattributes export-ignore
22
.github/ export-ignore
33
.gitignore export-ignore
4+
AGENTS.md export-ignore
45
ncs.* export-ignore
56
phpstan*.neon export-ignore
7+
docs/ export-ignore
68
tests/ export-ignore
79

810
*.php* diff=php

‎AGENTS.md‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# To My Agents!
2+
3+
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
4+
5+
## Documentation
6+
7+
Any distilled, agent-facing documentation for this package - how it works
8+
internally and the rationale behind key design decisions - lives in `docs/`.
9+
Consult it before non-trivial changes; it is the source of truth from which the
10+
public manual is distilled.
11+
12+
`Cache` -> `Storage` -> `Journal` is one layered mechanism whose traps only make
13+
sense together (overloaded `null`, dependency desugaring, the cross-method lock,
14+
the FileStorage format). Read `docs/internals.md` before editing them.
15+
16+
## Project Overview
17+
18+
Nette Caching provides flexible caching with pluggable storage backends
19+
(File, SQLite, Memcached, Memory, DevNull), rich dependency tracking (expiration,
20+
tags, files, callbacks), a PSR-16 adapter, and a Latte `{cache}` tag. Part of the
21+
Nette Framework, usable standalone.
22+
23+
- **PHP Version**: 8.1 - 8.5
24+
- **Package**: `nette/caching`
25+
26+
## Essential Commands
27+
28+
```bash
29+
# Run all tests
30+
vendor/bin/tester tests -s # or: composer tester
31+
32+
# Run one test directory / file
33+
vendor/bin/tester tests/Storages -s
34+
php tests/Caching/Cache.bulkLoad.phpt
35+
36+
# Static analysis (PHPStan level 8)
37+
composer phpstan
38+
```
39+
40+
## Conventions
41+
42+
- Every file starts with `declare(strict_types=1);`; Nette Coding Standard.
43+
- Tests are Nette Tester `.phpt` files under `tests/`; `tests/bootstrap.php`
44+
provides `test()` and `getTempDir()` (isolated per-process temp dir).
45+
- Constants are modern PascalCase (`Cache::Expire`) with deprecated UPPERCASE
46+
aliases (`Cache::EXPIRATION`) kept for BC; the `Storage` interface was `IStorage`
47+
before v3.1.
48+
49+
## Working in this repo
50+
51+
- **`null` is overloaded: `save($key, null)` *is* `remove()`.** `null` means cache
52+
miss on read, delete on write, and an already-expired `Expire` also deletes.
53+
Never add a path that stores `null` as a real value.
54+
- **Dependencies are desugared before storages see them** (`completeDependencies`):
55+
`Files`/`Constants` become `Callbacks`, `Expire` becomes a relative second count.
56+
Reason about a backend's support from the *post-desugar* set, not the public
57+
constant list.
58+
- **The lock spans two calls** - `Cache` locks before running a generator, and
59+
`write()`/`remove()` release it. **Stampede protection is real only in
60+
`FileStorage`**; every other backend's `lock()` is a no-op.
61+
- **`MemoryStorage` ignores all dependencies** (no expiration, no tags) - a real
62+
trap when it stands in for a real backend.
63+
- **FileStorage writes the real header last** (it doubles as a commit marker) and
64+
runs GC probabilistically **in the constructor**, not at shutdown.
65+
- User-facing how-to (DI wiring, storage/journal config, PSR-16 usage, `{cache}`
66+
tag) is manual material and lives in the public web docs, not here.

‎docs/internals.md‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Caching internals
2+
3+
How `nette/caching` works underneath, for agents editing it. Medium depth, one
4+
file: `Cache` → `Storage` → `Journal` is a single layered mechanism, and the traps
5+
(overloaded `null`, the cross-method lock, the FileStorage format) only make sense
6+
together.
7+
8+
## The three layers and where responsibility sits
9+
10+
- **`Cache`** is the high-level, storage-agnostic API: namespacing, dependency
11+
normalization, memoization, output capture. It never touches files or sockets.
12+
- **`Storage`** (interface) is the backend contract: `read`/`write`/`remove`/
13+
`lock`/`clean` (+ optional `BulkReader`/`BulkWriter`).
14+
- **`Journal`** (SQLite) is an **optional** side-index for tags and priorities.
15+
FileStorage needs one **only** for `Tags`/`Priority` and throws if a write uses
16+
them without a journal; everything else works journal-less.
17+
18+
Keys are namespaced by `Cache`: the namespace has a `NamespaceSeparator`
19+
(`"\x00"`) appended, and `generateKey` = `namespace . xxh128(key)`. `derive()`
20+
nests namespaces by concatenation. FileStorage later turns the `"\x00"` into a
21+
subdirectory (`getCacheFile`), so a namespace is a directory on disk.
22+
23+
## `null` is overloaded; `save($key, null)` **is** `remove()`
24+
25+
`remove()` is literally `save($key, null)`, and `null` carries three meanings:
26+
27+
- from `read()`/`load()` → **cache miss**;
28+
- as data passed to `save()` → **delete this key**;
29+
- an `Expire <= 0` (already expired) → **delete instead of write**.
30+
31+
So a generator that returns `null` does not cache a null — it removes the key.
32+
Do not add a code path that stores `null` as a value; the whole layer assumes
33+
`null` means absence.
34+
35+
## Dependencies are rewritten before they reach the storage
36+
37+
`completeDependencies()` normalizes the user's dependency array so that storages
38+
see a **smaller, uniform** set — this is why the "which backend supports which
39+
dependency" matrix is partly an illusion:
40+
41+
- **`Files` and `Constants` are desugared into `Callbacks`** (a `checkFile`/
42+
`checkConst` closure with an mtime/value snapshot). Storages never see `Files`
43+
or `Constants` — so those two work on **every** backend that supports
44+
`Callbacks`.
45+
- `Expire` is converted to a **relative second count** (`DateTime::from - time()`).
46+
- `Items` keys are run through `generateKey` (namespaced); `Tags`/`Namespaces`
47+
are flattened to lists.
48+
49+
What genuinely needs backend support is therefore narrower than it looks:
50+
`Tags`/`Priority` need a `Journal`, `Items` and `Sliding` need storage-level
51+
handling, and unsupported dependencies degrade (historically an
52+
`E_USER_WARNING`). An agent adding a backend must reason about this **post-desugar**
53+
set, not the public constant list.
54+
55+
## The lock spans two method calls (and is mostly a FileStorage thing)
56+
57+
The `Storage::lock` contract is explicit in the interface: *"Lock is released by
58+
`write()` or `remove()`."* This is the non-local invariant:
59+
60+
- `Cache::load`/`save` call `storage->lock($key)` **before** running a generator,
61+
then either `write()` (success) or `remove()` (generator threw) releases it.
62+
- `FileStorage::lock` opens the file `c+b`, takes `flock(LOCK_EX)`, and stores the
63+
handle in `$locks[$key]`; `write()` **reuses that same handle** (removing it from
64+
`$locks`) and unlocks at the end. The lock therefore lives across
65+
`lock → write`, not within one call.
66+
- **Stampede protection is real only for `FileStorage`.** Every other built-in
67+
storage (`MemoryStorage`, `Memcached`, `SQLite`, `DevNull`) implements `lock()`
68+
as a **no-op**, so concurrent `load($key, $generator)` calls can all generate at
69+
once there. "Nette caching prevents stampede" is a FileStorage guarantee, not a
70+
universal one.
71+
72+
## FileStorage on-disk format and atomic writes
73+
74+
The file layout is: **a 6-byte zero-padded meta-length header + serialized meta +
75+
data**. The write sequence is the subtle part and is a commit protocol:
76+
77+
1. under the held `LOCK_EX`, `ftruncate(0)`;
78+
2. write a **zero-filled** header placeholder, then the data;
79+
3. `fseek(0)` and write the **real** header **last**.
80+
81+
Writing the real (non-zero) header last means a reader that sees a non-zero size
82+
is guaranteed a complete record — the header doubles as the commit marker. Reads
83+
take `LOCK_SH`; the class docblock enumerates the three atomic operations
84+
(read/delete/write) and the **NTFS-vs-ext3 delete fallback**: `unlink` can fail on
85+
a locked NTFS file, so `delete()` falls back to `lock(EX) → truncate → close →
86+
unlink`.
87+
88+
Two more non-obvious behaviors:
89+
90+
- **Reads self-heal.** `verify()` checks expiration, sliding (`touch`es the file to
91+
extend it), callbacks, and — **recursively** — `Items` dependencies (each
92+
dependent file's `MetaTime` must still match). Any failure **deletes** the entry
93+
on the spot, so a stale read is also a cleanup.
94+
- **GC runs in the constructor, not at shutdown.** Each `FileStorage`
95+
instantiation runs `clean([])` with probability `$gcProbability` (0.001) —
96+
probabilistic garbage collection at construction time.
97+
98+
## Backends honor different subsets of dependencies
99+
100+
The built-in storages are `FileStorage`, `SQLiteStorage`, `MemcachedStorage`,
101+
`MemoryStorage`, and `DevNullStorage` (Redis and others live outside this
102+
package). They do **not** all honor the same dependencies, and the weakest are
103+
easy traps:
104+
105+
- **`MemoryStorage` ignores every dependency** — its `write()` stores the raw
106+
value and drops `$dependencies` entirely, so there is **no expiration, no tags,
107+
no invalidation** (only `clean(All)` empties it). It is a process-local map, fine
108+
for a single request, wrong for anything that must expire.
109+
- **`DevNullStorage`** discards everything (always a miss).
110+
- `FileStorage` is the reference implementation with the fullest support (via the
111+
meta header + optional `Journal`).
112+
113+
When adding or choosing a backend, verify the specific storage rather than
114+
assuming the public dependency constants are universally honored.
115+
116+
## Navigation map
117+
118+
| Concern | Where |
119+
|---|---|
120+
| Namespacing, key hashing | `Cache::generateKey`, `NamespaceSeparator`, `derive` |
121+
| `null` = remove, expire semantics | `Cache::save`, `remove` |
122+
| Dependency desugaring | `Cache::completeDependencies` |
123+
| Lock lifetime contract | `Storage::lock` (doc), `Cache::load`, `FileStorage::lock`/`write` |
124+
| On-disk format, commit-last header | `FileStorage::write`, `readMetaAndLock` |
125+
| Read-time verify + self-delete | `FileStorage::verify` |
126+
| Namespace→subdir, GC | `FileStorage::getCacheFile`, constructor |

0 commit comments

Comments
 (0)