66 lines
2.2 KiB
Go
66 lines
2.2 KiB
Go
package onboarding
|
|
|
|
import (
|
|
"context"
|
|
"data-terminal-cloud/internal/outbox"
|
|
"encoding/json"
|
|
"github.com/google/uuid"
|
|
"time"
|
|
)
|
|
|
|
// Recover durable jobs after an outbox retry budget was exhausted during a
|
|
// pause. ProcessActivation owns the device lock and the no-repeat fence.
|
|
func (s *Service) RunMaintenance(ctx context.Context) {
|
|
ticker := time.NewTicker(time.Minute)
|
|
defer ticker.Stop()
|
|
for {
|
|
if err := s.MaintainOnce(ctx); err != nil && ctx.Err() == nil {
|
|
s.options.Logger.Error("onboarding recovery scan failed")
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
func (s *Service) MaintainOnce(ctx context.Context) error {
|
|
rows, err := s.pool.Query(ctx, `SELECT j.device_id::text,j.id::text,j.binding_generation FROM device_activation_job j JOIN device d ON d.id=j.device_id JOIN device_binding b ON b.device_id=d.id AND b.unbound_at IS NULL AND b.generation=j.binding_generation WHERE d.status='registered' AND d.activation_state NOT IN ('LOST','REVOKED') AND j.binding_generation=d.binding_generation AND j.updated_at<now()-interval '3 minutes' AND (j.status IN ('NOT_STARTED','RESERVED','CONTACT_POSSIBLE') OR (j.status='REGISTERED_OBSERVED' AND j.config_ciphertext IS NULL)) ORDER BY j.updated_at LIMIT 10`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
type item struct {
|
|
DeviceID string `json:"device_id"`
|
|
ActivationID string `json:"activation_id"`
|
|
Generation int64 `json:"binding_generation"`
|
|
}
|
|
var items []item
|
|
for rows.Next() {
|
|
var v item
|
|
if err = rows.Scan(&v.DeviceID, &v.ActivationID, &v.Generation); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
items = append(items, v)
|
|
}
|
|
rows.Close()
|
|
if err = rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
if s.options.ActivationEnabled {
|
|
for _, v := range items {
|
|
raw, _ := json.Marshal(v)
|
|
if err = s.ProcessActivation(ctx, outbox.Event{ID: uuid.NewString(), Payload: raw}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
// Retain business attempts and idempotency tombstones for replay protection.
|
|
_, err = s.pool.Exec(ctx, `DELETE FROM device_handoff WHERE expires_at<now()-interval '1 day'`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.pool.Exec(ctx, `DELETE FROM device_time_challenge WHERE expires_at<now()-interval '1 day'`)
|
|
return err
|
|
}
|