All-Inclusive-Bundle
PilotWie Atomic-Activation ueber IAllInclusiveService funktioniert, was beim Plugin-Update-Sync passiert, und wie die TOCTOU-Race in Phase 2 geloest wurde.
Phase 2 · Pilot-Rollout
Was ist All-Inclusive?
Eine Lizenz-Variante, bei der der Kunde einen hoeheren Subscription-Preis zahlt und dafuer alle im Marketplace verfuegbaren Plugins automatisch inkludiert. Pro Plugin-Update wird die Inclusion-Liste aktualisiert — der Kunde bekommt neue Plugins ohne zusaetzlichen Kauf.
- Lizenzmodell:
license_model = all_inclusive - Persistenz:
all_inclusive_inclusionsmit(license_id, plugin_id)+ Unique-Index (Phase 2, Issue 22) - Stripe-Trigger:
priceId == PaymentOptions.PriceIdAllInclusivebeicustomer.subscription.created
Atomic-Activation (IAllInclusiveService)
Die Erst-Aktivierung laeuft in einer EF-Transaction, damit entweder alle Plugins inkludiert werden oder keiner. Pre-Check + Insert in einem Schritt.
public interface IAllInclusiveService
{
Task ActivateAllInclusiveAsync(
Guid licenseId,
CancellationToken ct
);
Task SyncNewPluginAsync(
Guid licenseId,
string pluginId,
CancellationToken ct
);
}TOCTOU-Race-Loss (Phase-2-Folge #1)
Problem: Zwei parallele Stripe-Webhooks fuer denselben Kunden (z.B. zwei Subscription-Updates in schneller Folge) konnten beide gleichzeitig den Pre-Check passieren und dann am Unique-Constraint scheitern — ohne dass der Hub-Operator den Fehler versteht.
Loesung (sellx_central ae1d552): Retry-on-Conflict-Loop mit IDbContextFactory, DB-agnostisch via IsUniqueViolation-Erkennung fuer SQLite / Postgres / SQL-Server. 3 Concurrency-Tests decken das Szenario ab.
// Pseudo: retry-on-conflict loop
for (int attempt = 1; attempt <= 5; attempt++)
{
try
{
await using var db = await _dbFactory.CreateDbContextAsync(ct);
await using var tx = await db.Database.BeginTransactionAsync(ct);
// Pre-Check + Insert in einer Transaction
if (!await db.AllInclusiveInclusions.AnyAsync(...))
{
db.AllInclusiveInclusions.Add(new Inclusion { ... });
await db.SaveChangesAsync(ct);
}
await tx.CommitAsync(ct);
return; // success
}
catch (DbUpdateException ex) when (IsUniqueViolation(ex))
{
// parallel insert won — retry on fresh context
continue;
}
}Plugin-Update-Sync
Wenn sellx_distribution ein neues Plugin veroeffentlicht, soll der All-Inclusive-Kunde es ohne zusaetzlichen Kauf erhalten. Dafuer gibt es IAllInclusiveService.SyncNewPluginAsync(licenseId, pluginId):
distribution-Publish triggert Central
Sobald ein neues .sellxpkg in sellx_distribution veroeffentlicht wird, ruft die distribution-Pipeline POST /api/v2/internal/plugin-published auf Central.
Fan-out an All-Inclusive-Lizenzen
Central ermittelt alle aktiven All-Inclusive-Licenses (background job) und ruft pro License SyncNewPluginAsync.
Idempotente Inclusion
Pro License + Plugin ein all_inclusive_inclusions-Eintrag. Unique-Index verhindert Doppelung. Bei bereits vorhandener Inclusion: no-op.
Hub holt Query beim naechsten Heartbeat
Der Hub sieht das neue Plugin im naechsten /api/v2/license/manifest-Response, laedt das verschluesselte .sql.enc herunter, entschluesselt mit HKDF (neuer Plugin-ID als Info).