Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- RSS and Atom feeds and the Categories widget use the Smarty block cache instead of having to go through the Smarty compiler every time. ([#864](https://github.com/flatpressblog/flatpress/pull/864))
- If APCu is available, the Smarty cache fragments are stored in the APCu cache instead of on disk. ([#865](https://github.com/flatpressblog/flatpress/pull/865))
- The sitemap returns an image from a post, if available. ([#940](https://github.com/flatpressblog/flatpress/pull/940))
- Post navigation now uses secure, absolute offset anchors with request, APCu, and file cache fallbacks ([#944](https://github.com/flatpressblog/flatpress/pull/944))

- Admin area:
- No typographic quotes in the plugin management. ([#874](https://github.com/flatpressblog/flatpress/pull/874))
Expand Down Expand Up @@ -40,6 +41,7 @@
- includes jQuery 4.0.0 and jQueryUI 1.14.2
- Archives plugin update to version 1.1.2 ([#896](https://github.com/flatpressblog/flatpress/pull/896))
- jQuery / JS legacy code have been updated
- The archives cache has been made language- and character set-safe ([#944](https://github.com/flatpressblog/flatpress/pull/944))
- Comment Center plugin update to version 1.1.5 ([#896](https://github.com/flatpressblog/flatpress/pull/896))
- jQuery / JS legacy code have been updated
- CookieBanner plugin update to version 1.0.4 ([#896](https://github.com/flatpressblog/flatpress/pull/896))
Expand Down
152 changes: 149 additions & 3 deletions docs/FlatPress_APCu_Cache_Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ Medium. All category link generation benefits, but categories change rarely.

- `@apcu_set($ckey, $lang, 0);` (no expiry).

**Locale setup performance note:**

- `set_locale()` now tries the configured locale/charset candidates directly with PHP `setlocale()`.
- Language selection, FlatPress language files, configured output charset, and the existing `setlocale()` fallback sequence remain independent of APCu.

**Impact:**
High on multi-language setups; otherwise medium.

Expand Down Expand Up @@ -345,6 +350,138 @@ Medium. Saves an include+parse of `settings.conf.php` on every request when `gen

---


### 2.10 Main Stream Offset Anchors – `fp:fpdb:offset-anchors:v1:*`

**Logical APCu prefix:** `fp:fpdb:offset-anchors:v1:<sha1(index_signature)>`
**Files:**

- `fp-includes/core/core.fpdb.class.php`
- `fp-includes/core/core.entry.php`
- `fp-includes/core/core.fileio.php`
- `fp-includes/core/core.apcu.php`

**Purpose:**

- Accelerates deep pages of the normal chronological entry stream.
- Stores only structural B+ tree navigation data:
- absolute entry-index offset,
- key at that offset,
- preceding key required to preserve `FPDB_Query::getPrevId()` semantics.
- It does **not** cache entry content, rendered HTML, comments, language output, or Smarty results.

**Why absolute offsets are used:**

`admin.php?p=config` can change `general.maxentries`, and callers can pass an explicit `count` or `start`.
The cache therefore never binds an anchor to a page number.

Examples:

- `page=9, count=5` -> absolute start offset `40`
- `page=3, count=20` -> absolute start offset `40`
- `start=40` -> absolute start offset `40`

All three can safely reuse the same structural anchor.

**Eligibility:**

The optimization is deliberately limited to the unfiltered main entry stream:

- main index/category `0`,
- no entry-ID query,
- no random query,
- no year/month/day filter,
- no category filter,
- no exclude index,
- positive `count`,
- start offset at or beyond the current anchor step.

All other query forms continue through the original B+ tree walker unchanged.

**Anchor spacing:**

- Default fixed step: `128`.
- Optional host override: `FP_FPDB_ANCHOR_STEP`.
- Accepted range is clamped to `16..4096`.
- For very large indexes the effective step grows automatically to keep the fixed anchor map at roughly no more than `8192` anchors.
- Optional switch `FP_FPDB_OFFSET_ANCHORS=0|false|off|no` disables the optimization completely.

**Validity signature:**

The main-index signature contains:

- cache schema version,
- `%%fpdb-index-generation.tmp` generation token,
- main index file mtime,
- main index file size,
- current B+ tree length.

`entry_index_generation_bump()` rotates the token only when the ordered main-index key set changes:

- a new main-index key is inserted,
- a main-index key is deleted.

Title-only and category-only edits do not move stream offsets and therefore do not force an unnecessary generation change.

The token is written through `io_write_file()`. If it cannot be created or safely rotated, offset-anchor reuse is disabled and FlatPress stays on the original walker.

**Backend and fallback order:**

```text
request-local anchor map
|
v
APCu namespaced hot cache
|
v
fp-content/cache/%%fpdb-offset-anchors-v1.json
|
v
original B+ tree walker
```

The pipes are intentionally aligned to make the fallback direction explicit.

- APCu is optional and uses the normal `is_apcu_on()`, `apcu_get()`, and `apcu_set()` wrappers.
- APCu TTL: `3600` seconds.
- The JSON fallback is regenerable runtime data and is written atomically through `io_write_file()`.
- Cache-file reads use `io_load_file_uncached()` because the anchor layer already provides its own request/APCu hierarchy and generation validation.
- If APCu is missing, full, disabled, or rejects a store, the file fallback remains available.
- If the file cache is missing, corrupt, read-only, or cannot be written, the B+ tree walker remains authoritative.
- Cache failures can reduce performance only; they must not change query results or entry write success.

**Warm-up behavior:**

- The first deep request for a new index generation still walks the original B+ tree and learns fixed-step anchors on that path.
- Following requests can start at the nearest verified anchor and walk only the remaining distance.
- Shallow pages below the anchor step do not perform generation or anchor-file lookups.

**Write and concurrency safety:**

- The B+ tree format and its insert/delete/rebalance implementation are unchanged.
- An anchor is accepted only when its key is still the exact walker key and the index signature remains unchanged after positioning.
- Newly learned anchors are persisted only if the signature still matches immediately before the write.
- The generation/cache files are not sources of truth and are safe to delete during cache maintenance.

**Language / charset safety:**

Offset anchors contain only index keys and offsets. They are independent of:

- FlatPress language,
- output charset,
- date/month translations,
- theme,
- Smarty,
- rendered content.

Changing language or charset therefore does not require anchor invalidation.

**Impact:**
Low on shallow pages by design; high on deep entry-stream pagination of large blogs after warm-up.

---


## 3. Plugin and Template Infrastructure Caches

### 3.1 Plugin Discovery and Status – `fp:plugin:*`, `fp:plugins:*`
Expand Down Expand Up @@ -481,8 +618,8 @@ Medium. Particularly useful when image metadata is frequently queried.
**Prefixes:**

- `fp:archives:v`
- `fp:archives:list:vN:<sig>`
- `fp:archives:html:vN:<sig>`
- `fp:archives:list:vN:loc-<sha1(lang|charset)>:<sig>`
- `fp:archives:html:vN:loc-<sha1(lang|charset)>:<sig>`

**File:** `fp-plugins/archives/plugin.archives.php`

Expand All @@ -493,6 +630,13 @@ Medium. Particularly useful when image metadata is frequently queried.

Both store BLOG_BASEURL as a placeholder `%BLOG_BASEURL%` and expand it on read.

**Language and charset isolation:**

- The APCu key contains `sha1(lowercase(lang) . '|' . lowercase(charset))`.
- The request-local archive cache uses the same locale/charset context.
- Switching the configured FlatPress language or output charset therefore cannot reuse a previously rendered month list from another locale.
- This matters because archive labels contain translated month names from the active FlatPress language data.

**Invalidation:**

- Namespaced by `fp:archives:v` (integer version in APCu).
Expand Down Expand Up @@ -1113,6 +1257,7 @@ The following table summarizes each logical cache group:
| Base URL Config | `fp:config:settings:*` | No | File mtime/size via `stat()`, TTL 1h | Medium |
| File I/O | `fp:io:*` | No | File mtime/size, TTL (default 1h) | High |
| Entries | `fp:entry:parsed:*` | No | Entry file mtime/size | High |
| Stream offset anchors | `fp:fpdb:offset-anchors:v1:*` | No | Main-index generation + mtime/size/length | High on deep pages |
| Comments | `fp:comments:list:*`, `fp:comments:count:*` | No | Comment dir mtime, TTL 300s (APCu) + file fallback | Medium–High |
| Static pages | `fp:statics:list:*` | No | Static dir mtime/size, TTL 600s | Medium |
| Categories | `fp:cats:list:*`, `fp:cats:encoded:*` | No | Categories file mtime/size, TTL 600s | Medium |
Expand All @@ -1125,7 +1270,7 @@ The following table summarizes each logical cache group:
| Smarty plugin index | `fp:spi:*` | No | Dir+token hash, TTL 300s | Medium |
| Search | `fp:search:rev`, `fp:search:v*` | No | Content rev + TTL (5s / 900s) | Medium |
| BBCode | `fp:bbcode:*` | No | Parser/img/meta mtimes, TTL 300–7200s | Medium–High |
| Archives | `fp:archives:v`, `fp:archives:list*`, `fp:archives:html*` | **Yes** | `plugin_archives_cache_bump()` + PrettyURLs bump | Medium |
| Archives | `fp:archives:v`, `fp:archives:list*`, `fp:archives:html*` | **Yes** | Generation/PrettyURLs bump + language/charset key | Medium |
| Calendar | `fp:calendar:v`, `calendar:*:vN` | **Yes** | `plugin_calendar_cache_bump()` + PrettyURLs bump | Medium–High |
| Storage plugin | `fp:storage:v`, `fp:storage:aggregate*`, `fp:storage:dirsize*`, `fp:storage:quota*` | No | Post-success hooks + generation bump + JSON purge/TTL | Medium–High |
| Mastodon instance snapshot | `fp:mastodon:instance_document:<sha1(instance_url)>` | No | TTL 900s, `instance_url` change, snapshot refresh | Low–Medium |
Expand Down Expand Up @@ -1160,6 +1305,7 @@ For completeness, the following logical prefixes are used by FlatPress `1.6.dev`
- `fp:comments:count:`
- `fp:config:settings:`
- `fp:entry:parsed:`
- `fp:fpdb:offset-anchors:v1:`
- `fp:https:v2:`
- `fp:ini:`
- `fp:io:`
Expand Down
74 changes: 71 additions & 3 deletions fp-includes/core/core.entry.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,68 @@
<?php

/**
* Returns the cache-generation token for the ordered main entry index.
*
* Offset anchors are read accelerators only. If the cache directory is not
* writable, returning false disables the optimization and the original
* B+ tree walker remains authoritative.
*
* @return string|false
*/
function entry_index_generation_read() {
$file = CACHE_DIR . '%%fpdb-index-generation.tmp';

// A read-only generation token cannot be rotated safely after writes.
// Disable anchor reuse instead of risking stale offsets.
if (is_file($file) && (!is_writable($file) || !is_writable(dirname($file)))) {
return false;
}

$raw = io_load_file_uncached($file);
if (is_string($raw) && trim($raw) !== '') {
return trim($raw);
}

try {
$random = bin2hex(random_bytes(12));
} catch (\Exception $e) {
$random = str_replace('.', '', uniqid('', true));
}
$token = sprintf('%.6F', microtime(true)) . '-' . $random;

if (!io_write_file($file, $token . "\n")) {
return false;
}

return $token;
}

/**
* Rotates the main entry-index generation after an ordered key-set mutation.
*
* A failed cache write must never make an entry save/delete fail. Removing
* the token disables offset-anchor reuse until a fresh token can be created.
*
* @return bool
*/
function entry_index_generation_bump() {
$file = CACHE_DIR . '%%fpdb-index-generation.tmp';

try {
$random = bin2hex(random_bytes(12));
} catch (\Exception $e) {
$random = str_replace('.', '', uniqid('', true));
}
$token = sprintf('%.6F', microtime(true)) . '-' . $random;

if (io_write_file($file, $token . "\n")) {
return true;
}

io_delete_file($file);
return false;
}

class entry_cached_index extends caching_SBPT {
// cache_filelister {
var $position = 0;
Expand All @@ -24,7 +87,6 @@ function __construct($id_cat = 0) {

$this->open();
}

}

class entry_index {
Expand Down Expand Up @@ -153,6 +215,7 @@ function add($id, $entry, $del = array(), $update_title = true) {

$main = & $this->get_index();
$seek = null;
$main_key_existed = ($main->has_key($key) !== false);

// title must not be updated, let's get the offset value from has_key
if (!$update_title) {
Expand Down Expand Up @@ -212,6 +275,12 @@ function add($id, $entry, $del = array(), $update_title = true) {
}
}

// Only insertion/removal of an ordered main-index key changes offsets.
// Title/category-only edits keep existing offset anchors valid.
if (!$main_key_existed) {
entry_index_generation_bump();
}

return $this->_lock_release();
}

Expand All @@ -237,9 +306,9 @@ function delete($id, $entry) {
}
}

entry_index_generation_bump();
return $this->_lock_release();
}

}

class entry_archives extends fs_filelister {
Expand Down Expand Up @@ -297,7 +366,6 @@ function getList() {
function getCount() {
return $this->_count;
}

}

/**
Expand Down
Loading