Failure Handling & Observability
Tại Sao Quan Trọng
Default behavior: Tasks fail → retry → exceed max attempts → silently dropped (unless DLQ).
Nếu không có observability:
- Bạn không biết tasks đang fail (silent)
- Bạn không biết rate (how many?)
- Bạn không biết why (underlying cause?)
- Bạn không recover (manual investigation at 3 AM)
Default Failure Behavior
Task Exceeds Max Attempts
Cloud Tasks config:
max_attempts: 100
max_backoff: 1 hour
Task fails on attempt 1:
Retry scheduled in 100ms
Retries continue:
Attempt 2, 3, ..., 99 all fail
Attempt 100 fails
Result:
Task deleted from queue (silently)
No alert, no notification
(Unless DLQ configured)Why Silent Dropout?
GCP philosophy: Tasks are "fire and forget" by default
- Publisher doesn't expect acknowledgment
- Pub/Sub has DLQ, but Tasks doesn't (by default)
- Operator must explicitly enable observabilityDead Letter Queue (DLQ)
Enable DLQ
go
queue := &Queue{
Name: "projects/my-project/locations/us-central1/queues/main",
DeadLetterConfig: &DeadLetterConfig{
DeadLetterQueue: "projects/my-project/locations/us-central1/queues/dead-letter",
MaxDeliveryAttempts: 5, // Move to DLQ after 5 attempts (instead of 100)
},
}
client.UpdateQueue(ctx, &UpdateQueueRequest{
Queue: queue,
})Flow
Task in "main" queue
↓ (fails)
Retry 1, 2, 3, 4
↓ (all fail, attempt 5 scheduled)
Task moved to "dead-letter" queue
↓
Operator inspects dead letter queue
↓
Investigate + fix root cause
↓
Re-enqueue task to "main" queueInspect Dead Letter Queue
bash
gcloud tasks list-tasks \
--queue=dead-letter \
--location=us-central1 \
--project=my-projectRe-enqueue from DLQ
bash
# Option 1: Copy task back to main queue
gcloud tasks create-http-task \
--queue=main \
--location=us-central1 \
--uri="https://handler.example.com" \
--message-body='...' \
--project=my-project
# Option 2: Manually investigate + fix, then re-createLogging & Monitoring
Cloud Logging Integration
Cloud Tasks automatically logs to Cloud Logging:
json
{
"timestamp": "2026-06-24T10:00:00Z",
"severity": "ERROR",
"logName": "projects/my-project/logs/cloudtasks.googleapis.com",
"resource": {
"type": "cloud_tasks_queue",
"labels": {
"queue_name": "projects/my-project/locations/us-central1/queues/my-queue",
"location_id": "us-central1"
}
},
"jsonPayload": {
"task_name": "projects/my-project/locations/us-central1/queues/my-queue/tasks/task-id",
"response_code": 500,
"dispatch_deadline": "2026-06-24T10:10:00Z",
"dispatch_time": "2026-06-24T10:00:00Z",
"attempt_number": 1,
"response_body": "Internal server error"
}
}Key Fields to Monitor
response_code: HTTP status from handler
200-299 → success
5xx → retry
attempt_number: Which retry (1 = first attempt)
dispatch_deadline: When task must complete (for App Engine)
response_body: Error message from handler
useful for debuggingQuery Failed Tasks
sql
SELECT
task_name,
attempt_number,
response_code,
dispatch_time,
response_body
FROM `my-project.cloudtasks_googleapis_com_cloud_tasks`
WHERE
severity = "ERROR"
AND resource.labels.queue_name = "projects/my-project/locations/us-central1/queues/my-queue"
AND timestamp > TIMESTAMP_SUB(NOW(), INTERVAL 1 HOUR)
AND response_code != 200 # Failed requests
ORDER BY timestamp DESC
LIMIT 100Alerting Patterns
Alert 1: Max Attempts Reached
python
# Create alert in Cloud Monitoring
alert_policy = {
"display_name": "Cloud Tasks Max Attempts Reached",
"conditions": [
{
"display_name": "Max attempts counter > 0",
"condition_threshold": {
"filter": """
resource.type = "cloud_tasks_queue"
AND metric.type = "cloudtasks.googleapis.com/task/attempt_count"
AND metric.labels.outcome = "max_attempts_exceeded"
""",
"comparison": "COMPARISON_GT",
"threshold_value": 0,
"duration": "300s", # 5 minutes
"aggregations": [
{
"alignment_period": "60s",
"per_series_aligner": "ALIGN_RATE",
}
],
},
}
],
"notification_channels": ["email@example.com"],
}Action: Investigate handler, logs, data integrity.
Alert 2: High Failure Rate
python
alert_policy = {
"display_name": "Cloud Tasks High Failure Rate",
"conditions": [
{
"display_name": "Failure rate > 5%",
"condition_threshold": {
"filter": """
resource.type = "cloud_tasks_queue"
AND metric.type = "cloudtasks.googleapis.com/queue/task_attempt_count"
""",
"comparison": "COMPARISON_GT",
"threshold_value": 0.05, # 5%
"duration": "300s",
},
}
],
}Action: Check handler health, dependencies (DB, API), rate limits.
Alert 3: Task Age (Queue Backlog)
python
# Tasks pending for > 1 hour = backlog
alert_policy = {
"display_name": "Cloud Tasks Queue Backlog",
"conditions": [
{
"display_name": "Oldest task age > 1 hour",
"custom_query": """
fetch cloud_tasks_queue
| metric 'cloudtasks.googleapis.com/queue/task_count'
| value [val('task_count')]
| condition val('task_count') > 1000
&& resource.queue_name == 'projects/my-project/...'
""",
}
],
}Action: Queue dispatcher might be throttled, increase rate limits or add queues.
Observability in Handler
Structured Logging
go
import "cloud.google.com/go/logging"
func PaymentHandler(w http.ResponseWriter, r *http.Request) {
client := logging.NewClient(r.Context(), projectID)
defer client.Close()
// Extract task metadata
taskName := r.Header.Get("X-CloudTasks-TaskName")
retryCount := r.Header.Get("X-CloudTasks-TaskRetryCount")
// Parse payload
var req PaymentRequest
json.NewDecoder(r.Body).Decode(&req)
// Log with context
client.Logger("payment-handler").Log(logging.Entry{
Severity: logging.Info,
Payload: map[string]interface{}{
"task_name": taskName,
"retry_count": retryCount,
"payment_id": req.PaymentID,
"amount": req.Amount,
"status": "processing",
},
})
// Process
result, err := chargePayment(r.Context(), req)
if err != nil {
client.Logger("payment-handler").Log(logging.Entry{
Severity: logging.Error,
Payload: map[string]interface{}{
"task_name": taskName,
"retry_count": retryCount,
"payment_id": req.PaymentID,
"error": err.Error(),
"error_type": fmt.Sprintf("%T", err),
},
})
w.WriteHeader(http.StatusInternalServerError)
return
}
client.Logger("payment-handler").Log(logging.Entry{
Severity: logging.Info,
Payload: map[string]interface{}{
"task_name": taskName,
"retry_count": retryCount,
"payment_id": req.PaymentID,
"status": "completed",
"transaction_id": result.TransactionID,
},
})
w.WriteHeader(http.StatusOK)
}Metrics in Handler
go
import "cloud.google.com/go/monitoring/apiv3/v2"
func PaymentHandler(w http.ResponseWriter, r *http.Request) {
metric := &monitoring.MetricDescriptor{
Type: "custom.googleapis.com/payment/charges",
Labels: []*label.LabelDescriptor{
{Key: "status", ValueType: label.LabelDescriptor_STRING},
},
}
// Try to charge
err := chargePayment()
// Record metric
status := "success"
if err != nil {
status = "failure"
}
timeSeriesData := &monitoring.TimeSeries{
Metric: &metric.Type.Metric{
Type: "custom.googleapis.com/payment/charges",
Labels: map[string]string{
"status": status,
},
},
Points: []*monitoring.Point{
{
Interval: &monitoring.TimeInterval{
EndTime: ×tamp.Timestamp{
Seconds: time.Now().Unix(),
},
},
Value: &monitoring.TypedValue{
Value: &monitoring.TypedValue_Int64Value{
Int64Value: 1,
},
},
},
},
}
// Send to Cloud Monitoring
client.CreateTimeSeries(ctx, &monitoring.CreateTimeSeriesRequest{
Name: fmt.Sprintf("projects/%s", projectID),
TimeSeries: []*monitoring.TimeSeries{timeSeriesData},
})
}Troubleshooting Patterns
Pattern 1: Task Stuck in Queue
Symptom: Task age > 1 hour, not dispatched
Diagnosis:
1. Check queue state: is it RUNNING or PAUSED?
2. Check rate limits: dispatch_rate = 0?
3. Check handler: returning 503 (Service Unavailable)?
4. Check network: connectivity to handler endpoint?Pattern 2: All Tasks Failing
Symptom: 100% failure rate
Diagnosis:
1. Check handler logs: what error being returned?
2. Check handler code: did deployment introduce bug?
3. Check dependencies: is database down, API unavailable?
4. Check network: connectivity, firewall rules?
Debug:
curl -v https://handler.example.com/webhook \
-H "Content-Type: application/json" \
-d '{"test": "data"}'Pattern 3: Cascade Failure
Symptom: Initial handler failure → retry storm → more handlers down
Root cause: Backoff too short, retry rate too high
Diagnosis:
1. Check retry config: min_backoff, max_backoff, max_doublings
2. Check queue rate: max_dispatches_per_second, max_concurrent_dispatches
3. Check handler capacity: can it handle retry load?
Fix:
1. Increase backoff (min 1 second, max 1 hour)
2. Decrease concurrent (reduce concurrent_dispatches)
3. Pause queue during incident recoveryPattern 4: Duplicate Execution
Symptom: Data appears twice (payments charged twice)
Root cause: Handler not idempotent
Diagnosis:
1. Check handler code: does it check for duplicates?
2. Check logs: how many times was it called?
Fix:
1. Implement idempotent handler (check database first)
2. Use deduplication key (trace ID in database)
3. Add unique constraint to prevent duplicate dataRecovery Procedures
Incident: Handler Crashed
Step 1: Pause queue (stop new dispatches)
gcloud tasks queues update my-queue --pause
Step 2: Investigate handler logs
Check application logs, errors, root cause
Step 3: Deploy fix
Fix code, redeploy handler
Step 4: Resume queue
gcloud tasks queues update my-queue --resume
Step 5: Monitor
Watch failure rate, retry rate, task age
Adjust rate limits if neededIncident: Database Down
Step 1: Pause queue
Step 2: Wait for database recovery (or restore)
Step 3: Test handler manually
curl https://handler.example.com/test
Step 4: Resume queue (tasks start retrying)
Step 5: Monitor recovery
Ensure backlog drains (task age decreases)
Ensure failure rate drops to 0%Incident: Permanent Data Corruption
Step 1: Pause queue (don't make things worse)
Step 2: Investigate failed tasks (DLQ)
Which tasks failed? Why?
Step 3: Fix data manually (or rollback)
Restore from backup, fix inconsistencies
Step 4: Resume queue
Step 5: Optional: Re-enqueue failed tasks from DLQ
If new attempts would succeed with fixed data