Per-channel publish interval #130

Open
opened 2026-08-10 21:56:33 +02:00 by myrmidex · 0 comments
Owner

Summary

Let each channel override the global article_publishing_interval, so a community that tolerates frequent posts isn't held to the pace of one that doesn't.

Split out of #90 "Publishing schedule control", which bundled four independent features.

Current behaviour

PublishNextArticleJob gates on a global last-published timestamp:

$lastPublishedAt = ArticlePublication::max('published_at');   // all channels
if ($lastPublishedAt && now()->diffInMinutes($lastPublishedAt, true) < $interval) return;

One slow channel therefore paces every other channel.

Design

Add a nullable platform_channels.publish_interval_minutes. null = use the global setting.

Channel level, not route level. A route is a feed→channel pair, so a route-level override would give two feeds routed to the same channel a separate budget each — and the channel could still be flooded. The community is what gets flooded, so the limit belongs where the posts land.

article_publications.platform_channel_id already exists, so the per-channel "last published" query needs no schema change beyond the override column.

Head-of-line blocking — the main risk

PublishNextArticleJob selects the oldest eligible route article and publishes one per run. If that article's channel is inside its interval and the job simply returns, every other channel stalls behind it until the throttled one is ready.

This is the same head-of-line problem already documented for failing publishes (see #119 and .claude/PLATFORM.md).

A throttled channel must be skipped over — the job should fall through to the oldest candidate whose channel is free — not block the queue. This needs an explicit test: a throttled channel must not delay a non-throttled one.

Tasks

  • Migration: nullable platform_channels.publish_interval_minutes
  • Model cast + factory state
  • Per-channel interval check in PublishNextArticleJob, falling back to the global setting
  • Skip throttled channels instead of returning
  • Channels UI field (dark mode from the start — see #88)
  • API surface on the channel endpoints
  • Tests

Tests that matter

  • Channel with an override vs channel without, both eligible in the same run
  • Throttled channel does not block a free channel (head-of-line regression guard)
  • null override = global interval, i.e. today's behaviour exactly
  • Override of 0 = no pacing for that channel

Constraints

Do not weaken #123 (Cache::lock duplicate guard) or #119 (scopeDueForPublishing is the single source of truth for publish eligibility — do not re-express that threshold in a new query).

## Summary Let each channel override the global `article_publishing_interval`, so a community that tolerates frequent posts isn't held to the pace of one that doesn't. Split out of #90 "Publishing schedule control", which bundled four independent features. ## Current behaviour `PublishNextArticleJob` gates on a **global** last-published timestamp: ```php $lastPublishedAt = ArticlePublication::max('published_at'); // all channels if ($lastPublishedAt && now()->diffInMinutes($lastPublishedAt, true) < $interval) return; ``` One slow channel therefore paces every other channel. ## Design Add a nullable `platform_channels.publish_interval_minutes`. `null` = use the global setting. **Channel level, not route level.** A route is a feed→channel pair, so a route-level override would give two feeds routed to the same channel a separate budget each — and the channel could still be flooded. The community is what gets flooded, so the limit belongs where the posts land. `article_publications.platform_channel_id` already exists, so the per-channel "last published" query needs no schema change beyond the override column. ## Head-of-line blocking — the main risk `PublishNextArticleJob` selects the **oldest** eligible route article and publishes **one** per run. If that article's channel is inside its interval and the job simply returns, every other channel stalls behind it until the throttled one is ready. This is the same head-of-line problem already documented for failing publishes (see #119 and `.claude/PLATFORM.md`). A throttled channel must be **skipped over** — the job should fall through to the oldest candidate whose channel is free — not block the queue. This needs an explicit test: a throttled channel must not delay a non-throttled one. ## Tasks - [ ] Migration: nullable `platform_channels.publish_interval_minutes` - [ ] Model cast + factory state - [ ] Per-channel interval check in `PublishNextArticleJob`, falling back to the global setting - [ ] Skip throttled channels instead of returning - [ ] Channels UI field (dark mode from the start — see #88) - [ ] API surface on the channel endpoints - [ ] Tests ## Tests that matter - Channel with an override vs channel without, both eligible in the same run - Throttled channel does **not** block a free channel (head-of-line regression guard) - `null` override = global interval, i.e. today's behaviour exactly - Override of `0` = no pacing for that channel ## Constraints Do not weaken #123 (`Cache::lock` duplicate guard) or #119 (`scopeDueForPublishing` is the single source of truth for publish eligibility — do not re-express that threshold in a new query).
myrmidex added this to the v2.0.0 milestone 2026-08-10 21:56:33 +02:00
myrmidex added the
enhancement
label 2026-08-10 21:56:33 +02:00
myrmidex removed this from the v2.0.0 milestone 2026-08-15 12:56:01 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/fedi-feed-router#130
No description provided.