Skip to content

Latest commit

 

History

History
594 lines (467 loc) · 11.8 KB

File metadata and controls

594 lines (467 loc) · 11.8 KB

FASE 0.1 - QUICK REFERENCE GUIDE

Uso: Consulta rapida durante implementacao Documento completo: FASE_0.1_DOCUMENTACAO_EXTERNA.md


POSTGRESQL + GORM

Connection String (Production)

dsn := "host=10.1.2.3 user=app password=secret dbname=typecraft port=5432 sslmode=verify-full sslrootcert=/etc/ssl/certs/server-ca.pem TimeZone=UTC"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
    TranslateError: true, // IMPORTANTE
})

sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(25)
sqlDB.SetConnMaxLifetime(5 * time.Minute)

SSL Modes

disable      - NUNCA em producao
require      - Evitar (vulnerable to MITM)
verify-ca    - OK para private CA
verify-full  - OBRIGATORIO para producao (Cloud SQL, RDS)

Error Handling

if errors.Is(err, gorm.ErrRecordNotFound) { /* not found */ }
if errors.Is(err, gorm.ErrDuplicatedKey) { /* unique violation */ }
if errors.Is(err, gorm.ErrForeignKeyViolated) { /* FK violation */ }

Migration Strategy

Development:  AutoMigrate
Staging:      Versioned migrations (gormigrate/Atlas)
Production:   Versioned migrations + backup + testing

REDIS + ASYNQ

Client Setup

client := asynq.NewClient(asynq.RedisClientOpt{
    Addr:     "localhost:6379",
    Password: "",
    DB:       0,
})
defer client.Close()

task, _ := NewEmailTask(userID, email)
client.Enqueue(task, asynq.MaxRetry(3), asynq.Timeout(30*time.Second))

Server Setup

srv := asynq.NewServer(
    redisOpt,
    asynq.Config{
        Concurrency: 10,
        Queues: map[string]int{
            "critical": 6,
            "default":  3,
            "low":      1,
        },
    },
)

Task Handler

func HandleTask(ctx context.Context, t *asynq.Task) error {
    var p Payload
    if err := json.Unmarshal(t.Payload(), &p); err != nil {
        return fmt.Errorf("unmarshal failed: %w", asynq.SkipRetry)
    }

    // Process task
    if err := process(p); err != nil {
        return err // Will retry
    }

    return nil // Success
}

Redis Config (redis.conf)

appendonly yes
appendfsync everysec
maxmemory-policy noeviction

GIN FRAMEWORK

Middleware Order

router := gin.New()
router.Use(corsMiddleware())       // 1. CORS (preflight)
router.Use(gin.Recovery())         // 2. Panic recovery
router.Use(gin.Logger())           // 3. Logging
router.Use(rateLimitMiddleware())  // 4. Rate limit
router.Use(authMiddleware())       // 5. Authentication

CORS (Production)

import "github.com/gin-contrib/cors"

router.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"https://app.typecraft.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
    AllowCredentials: true,
    MaxAge:           12 * time.Hour,
}))

Error Handling

// Use AbortWithStatusJSON for errors
if err := validate(); err != nil {
    c.AbortWithStatusJSON(400, gin.H{"error": err.Error()})
    return
}

Graceful Shutdown

srv := &http.Server{
    Addr:         ":" + os.Getenv("PORT"),
    Handler:      router,
    ReadTimeout:  10 * time.Second,
    WriteTimeout: 10 * time.Second,
}

go srv.ListenAndServe()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)

Health Checks

// Liveness
router.GET("/healthz", func(c *gin.Context) {
    c.JSON(200, gin.H{"status": "ok"})
})

// Readiness
router.GET("/readyz", func(c *gin.Context) {
    if err := db.Exec("SELECT 1").Error; err != nil {
        c.JSON(503, gin.H{"status": "not ready"})
        return
    }
    c.JSON(200, gin.H{"status": "ready"})
})

CHROMEDP

Basic Setup

opts := append(chromedp.DefaultExecAllocatorOptions[:],
    chromedp.NoSandbox,          // Docker REQUIRED
    chromedp.DisableDevShmUsage, // Docker REQUIRED
    chromedp.DisableGPU,
)

allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()

browserCtx, cancel := chromedp.NewContext(allocCtx)
defer cancel()

// Initialize (NO TIMEOUT)
chromedp.Run(browserCtx, chromedp.Navigate("about:blank"))

PDF Generation

func generatePDF(url string) ([]byte, error) {
    ctx, cancel := context.WithTimeout(browserCtx, 30*time.Second)
    defer cancel()

    var buf []byte
    err := chromedp.Run(ctx,
        chromedp.Navigate(url),
        chromedp.WaitReady("body"),
        chromedp.Sleep(1*time.Second), // Safety margin for images
        chromedp.ActionFunc(func(ctx context.Context) error {
            var err error
            buf, _, err = page.PrintToPDF().
                WithPrintBackground(true).
                WithPaperWidth(8.27).
                WithPaperHeight(11.69).
                WithMarginTop(0.4).
                WithMarginBottom(0.4).
                WithMarginLeft(0.4).
                WithMarginRight(0.4).
                Do(ctx)
            return err
        }),
    )

    return buf, err
}

Dockerfile

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o app

FROM chromedp/headless-shell:latest
COPY --from=builder /app/app /app
USER chromedp
EXPOSE 8080
ENTRYPOINT ["/app"]

DOCKER / CLOUD RUN

Multi-Stage Build

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -trimpath -ldflags="-s -w" -o app

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]

Cloud Run Service (cloud-run.yaml)

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: typecraft
spec:
  template:
    metadata:
      annotations:
        run.googleapis.com/timeout: '300s'
        autoscaling.knative.dev/maxScale: '10'
    spec:
      containers:
      - image: gcr.io/PROJECT_ID/typecraft:latest
        env:
        - name: PORT
          value: "8080"
        - name: GIN_MODE
          value: "release"
        - name: INSTANCE_CONNECTION_NAME
          value: "project:region:instance"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-password
              key: latest
        resources:
          limits:
            memory: 512Mi
            cpu: '1'
        startupProbe:
          httpGet:
            path: /healthz
            port: 8080
          periodSeconds: 10
          timeoutSeconds: 1
          failureThreshold: 3

Cloud SQL Connection

// Unix Socket (Recommended)
dsn := fmt.Sprintf("host=/cloudsql/%s user=%s password=%s dbname=%s sslmode=disable",
    os.Getenv("INSTANCE_CONNECTION_NAME"),
    os.Getenv("DB_USER"),
    os.Getenv("DB_PASSWORD"),
    os.Getenv("DB_NAME"),
)

// Private IP (VPC)
dsn := fmt.Sprintf("host=%s port=5432 user=%s password=%s dbname=%s sslmode=require",
    os.Getenv("DB_HOST"),
    os.Getenv("DB_USER"),
    os.Getenv("DB_PASSWORD"),
    os.Getenv("DB_NAME"),
)

Deploy

# Build
gcloud builds submit --tag gcr.io/PROJECT_ID/typecraft

# Deploy
gcloud run services replace cloud-run.yaml --region=us-central1

STRIPE

Initialize

import "github.com/stripe/stripe-go/v76"

stripe.Key = os.Getenv("STRIPE_SECRET_KEY")

Webhook Handler

func handleWebhook(c *gin.Context) {
    payload, _ := ioutil.ReadAll(c.Request.Body)
    sig := c.GetHeader("Stripe-Signature")
    secret := os.Getenv("STRIPE_WEBHOOK_SECRET")

    event, err := webhook.ConstructEvent(payload, sig, secret)
    if err != nil {
        c.JSON(400, gin.H{"error": "invalid signature"})
        return
    }

    c.JSON(200, gin.H{"received": true})
    go processWebhookEvent(event)
}

Event Processing

func processWebhookEvent(event stripe.Event) {
    switch event.Type {
    case "payment_intent.succeeded":
        var pi stripe.PaymentIntent
        json.Unmarshal(event.Data.Raw, &pi)

        // IDEMPOTENT check
        var order Order
        if db.Where("stripe_payment_intent_id = ? AND status = ?", pi.ID, "paid").First(&order).Error == nil {
            return // Already processed
        }

        db.Model(&Order{}).Where("stripe_payment_intent_id = ?", pi.ID).Update("status", "paid")
    }
}

Idempotency Keys

import "github.com/google/uuid"

params := &stripe.PaymentIntentParams{
    Amount:   stripe.Int64(1000),
    Currency: stripe.String("usd"),
}
params.IdempotencyKey = stripe.String(uuid.New().String())

pi, err := paymentintent.New(params)

Error Handling

if err != nil {
    if stripeErr, ok := err.(*stripe.Error); ok {
        switch stripeErr.Type {
        case stripe.ErrorTypeCard:
            // Don't retry
        case stripe.ErrorTypeRateLimit:
            // Retry with backoff
        }
    }
}

CONFIGURATION VALUES REFERENCE

PostgreSQL Connection Pool

Small/Medium App:
  SetMaxIdleConns: 10
  SetMaxOpenConns: 25
  ConnMaxLifetime: 5 * time.Minute

High Traffic:
  SetMaxIdleConns: 25
  SetMaxOpenConns: 100
  ConnMaxLifetime: 5 * time.Minute

Asynq Concurrency

CPU-bound:  runtime.NumCPU()
I/O-bound:  runtime.NumCPU() * 2
Production: 10-50

Cloud Run Timeouts

Request timeout (Services):
  Default: 300s (5 min)
  Max:     3600s (1 hour)

Task timeout (Jobs):
  Default: 600s (10 min)
  Max:     604800s (7 days)

Health check timeout:
  Default: 1s
  Max:     240s

Chromedp Timeouts

Browser allocation: NO TIMEOUT
PDF generation:     30s
Page navigation:    30s
Element wait:       10s

Paper Sizes (inches)

A4:      8.27 x 11.69
Letter:  8.5 x 11
Legal:   8.5 x 14
A3:      11.69 x 16.54

COMMON PITFALLS

GORM

  • Forgetting TranslateError: true
  • Using == instead of errors.Is()
  • Not setting connection pool limits
  • sslmode=disable in production

Asynq

  • Not wrapping unmarshal errors with SkipRetry
  • AOF disabled (data loss)
  • No idempotency in handlers
  • Concurrency too high

Gin

  • AllowAllOrigins with AllowCredentials
  • c.JSON for errors (should use Abort)
  • No graceful shutdown
  • Heavy health checks

Chromedp

  • Timeout on first Run()
  • Creating new browser per request
  • No wait before PrintToPDF
  • Missing Docker flags (NoSandbox, DisableDevShmUsage)

Cloud Run

  • Hardcoded port (should read PORT env var)
  • Secrets in env vars (should use Secret Manager)
  • MaxOpenConns too high (* num_instances)
  • Request timeout too short for workload

Stripe

  • No signature verification
  • Blocking webhook response
  • Reusing idempotency keys
  • Non-idempotent handlers

ENVIRONMENT VARIABLES CHECKLIST

# Application
PORT=8080
GIN_MODE=release

# Database
DB_HOST=10.1.2.3
DB_PORT=5432
DB_NAME=typecraft
DB_USER=app
DB_PASSWORD=<from-secret-manager>

# Cloud SQL
INSTANCE_CONNECTION_NAME=project:region:instance

# Redis
REDIS_ADDR=localhost:6379
REDIS_PASSWORD=
REDIS_DB=0

# Stripe
STRIPE_SECRET_KEY=sk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx

# Logging
LOG_LEVEL=info

DEPLOYMENT CHECKLIST

Pre-Deploy

  • Multi-stage Dockerfile
  • Non-root user
  • All secrets in Secret Manager
  • Health checks implemented
  • Graceful shutdown implemented
  • Connection pooling configured

Deploy

  • Build image: gcloud builds submit
  • Deploy service: gcloud run services replace
  • Verify health checks pass
  • Test endpoints
  • Monitor logs
  • Check metrics

Post-Deploy

  • Verify DB connections
  • Test Stripe webhooks
  • Monitor error rates
  • Check Asynq queue depth
  • Verify PDF generation works

FIM DA QUICK REFERENCE

Para detalhes completos, consultar: FASE_0.1_DOCUMENTACAO_EXTERNA.md