EntwicklerMitarbeiter-Handbuch

All-Inclusive-Bundle

Pilot

Wie 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_inclusions mit (license_id, plugin_id) + Unique-Index (Phase 2, Issue 22)
  • Stripe-Trigger: priceId == PaymentOptions.PriceIdAllInclusive bei customer.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.

C#IAllInclusiveService.cs
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.

C#retry-pattern.cs
// 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):

1

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.

2

Fan-out an All-Inclusive-Lizenzen

Central ermittelt alle aktiven All-Inclusive-Licenses (background job) und ruft pro License SyncNewPluginAsync.

3

Idempotente Inclusion

Pro License + Plugin ein all_inclusive_inclusions-Eintrag. Unique-Index verhindert Doppelung. Bei bereits vorhandener Inclusion: no-op.

4

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).