Move tracker policy rendering out of write transaction

PublishTrackerPolicy ran template execution, markdown parsing and JSON
marshaling inside the WithTx callback, holding a write transaction open
across CPU-bound work. Split it into a read phase (WithConn) that
gathers data and renders the document, followed by a write phase that
persists the document and version, mirroring PublishThirdPartyList.

Also sort the generated third-party rows before returning. LoadByIDs
has no ORDER BY, so the policy document could otherwise be emitted in a
different row order on each regeneration.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-02 10:08:05 +02:00
parent 73854f98cb
commit 6eff9ecb97

View File

@@ -20,6 +20,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"text/template"
"time"
@@ -73,27 +74,54 @@ func (s *GeneratedDocumentService) PublishTrackerPolicy(
scope coredata.Scoper,
cookieBannerID gid.GID,
) error {
// Phase 1: collect data and render the prosemirror document outside any
// write transaction. Template rendering, markdown parsing, and JSON
// marshaling are CPU work that should not hold a write transaction open.
var (
organizationID gid.GID
bannerOrigin string
documentData docgen.TrackerPolicyData
)
err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
banner := &coredata.CookieBanner{}
if err := banner.LoadByID(ctx, conn, scope, cookieBannerID); err != nil {
return fmt.Errorf("cannot load cookie banner: %w", err)
}
organization := &coredata.Organization{}
if err := organization.LoadByID(ctx, conn, scope, banner.OrganizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
var err error
documentData, err = s.buildTrackerPolicyDocumentData(ctx, scope, conn, organization, banner)
if err != nil {
return fmt.Errorf("cannot build document data: %w", err)
}
organizationID = banner.OrganizationID
bannerOrigin = banner.Origin
return nil
})
if err != nil {
return err
}
prosemirrorJSON, err := BuildTrackerPolicyDocument(documentData)
if err != nil {
return fmt.Errorf("cannot build prosemirror document: %w", err)
}
// Phase 2: persist the document and version in a write transaction.
return s.svc.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
banner := &coredata.CookieBanner{}
if err := banner.LoadByID(ctx, tx, scope, cookieBannerID); err != nil {
return fmt.Errorf("cannot load cookie banner: %w", err)
}
organization := &coredata.Organization{}
if err := organization.LoadByID(ctx, tx, scope, banner.OrganizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
documentData, err := s.buildTrackerPolicyDocumentData(ctx, scope, tx, organization, banner)
if err != nil {
return fmt.Errorf("cannot build document data: %w", err)
}
prosemirrorJSON, err := BuildTrackerPolicyDocument(documentData)
if err != nil {
return fmt.Errorf("cannot build prosemirror document: %w", err)
return fmt.Errorf("cannot reload cookie banner: %w", err)
}
now := time.Now()
@@ -128,7 +156,7 @@ func (s *GeneratedDocumentService) PublishTrackerPolicy(
document = &coredata.Document{
ID: documentID,
OrganizationID: banner.OrganizationID,
OrganizationID: organizationID,
WriteMode: coredata.DocumentWriteModeGenerated,
TrustCenterVisibility: coredata.TrustCenterVisibilityPrivate,
Status: coredata.DocumentStatusActive,
@@ -153,9 +181,9 @@ func (s *GeneratedDocumentService) PublishTrackerPolicy(
documentVersionID := gid.New(scope.GetTenantID(), coredata.DocumentVersionEntityType)
documentVersion := &coredata.DocumentVersion{
ID: documentVersionID,
OrganizationID: banner.OrganizationID,
OrganizationID: organizationID,
DocumentID: document.ID,
Title: fmt.Sprintf("Cookie and Tracking Technologies Policy — %s", banner.Origin),
Title: fmt.Sprintf("Cookie and Tracking Technologies Policy — %s", bannerOrigin),
Content: prosemirrorJSON,
Classification: coredata.DocumentClassificationPublic,
DocumentType: coredata.DocumentTypePolicy,
@@ -164,7 +192,7 @@ func (s *GeneratedDocumentService) PublishTrackerPolicy(
UpdatedAt: now,
}
return s.publishOrRequestApproval(ctx, scope, tx, document, documentVersion, banner.OrganizationID, nil, false, now)
return s.publishOrRequestApproval(ctx, scope, tx, document, documentVersion, organizationID, nil, false, now)
},
)
}
@@ -262,6 +290,21 @@ func (s *GeneratedDocumentService) buildTrackerPolicyThirdParties(
rows = append(rows, row)
}
// LoadByIDs returns rows in an unspecified order (no ORDER BY), so sort
// here to keep the generated policy document deterministic across
// regenerations.
slices.SortFunc(rows, func(a, b docgen.TrackerPolicyThirdParty) int {
if c := strings.Compare(a.Name, b.Name); c != 0 {
return c
}
if c := strings.Compare(a.Description, b.Description); c != 0 {
return c
}
return strings.Compare(a.PrivacyPolicyURL, b.PrivacyPolicyURL)
})
return rows, nil
}