package sql import ( "context" "fmt" "net/http" "os" "sync" "time" "github.com/fullstorydev/grpchan/inprocgrpc" "github.com/google/uuid" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/parquet" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/db" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" "github.com/grafana/grafana/pkg/storage/unified/sql/dbutil" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) var ( _ resource.BulkProcessingBackend = (*backend)(nil) ) type bulkRV struct { max int64 counter int64 } // When executing a bulk import we can fake the RV values func newBulkRV() *bulkRV { t := time.Now().Truncate(time.Second * 10) return &bulkRV{ max: (t.UnixMicro() / 10000000) * 10000000, counter: 0, } } func (x *bulkRV) next(obj metav1.Object) int64 { ts := obj.GetCreationTimestamp().UnixMicro() anno := obj.GetAnnotations() if anno != nil { v := anno[utils.AnnoKeyUpdatedTimestamp] t, err := time.Parse(time.RFC3339, v) if err == nil { ts = t.UnixMicro() } } if ts > x.max || ts < 10000000 { ts = x.max } x.counter++ return (ts/10000000)*10000000 + x.counter } type bulkLock struct { running map[string]bool mu sync.Mutex } func (x *bulkLock) Start(keys []*resourcepb.ResourceKey) error { x.mu.Lock() defer x.mu.Unlock() // First verify that it is not already running ids := make([]string, len(keys)) for i, k := range keys { id := resource.NSGR(k) if x.running[id] { return &apierrors.StatusError{ErrStatus: metav1.Status{ Code: http.StatusPreconditionFailed, Message: "bulk export is already running", }} } ids[i] = id } // Then add the keys to the lock for _, k := range ids { x.running[k] = true } return nil } func (x *bulkLock) Finish(keys []*resourcepb.ResourceKey) { x.mu.Lock() defer x.mu.Unlock() for _, k := range keys { delete(x.running, resource.NSGR(k)) } } func (x *bulkLock) Active() bool { x.mu.Lock() defer x.mu.Unlock() return len(x.running) > 0 } func (b *backend) ProcessBulk(ctx context.Context, setting resource.BulkSettings, iter resource.BulkRequestIterator) *resourcepb.BulkResponse { err := b.bulkLock.Start(setting.Collection) if err != nil { return &resourcepb.BulkResponse{ Error: resource.AsErrorResult(err), } } defer b.bulkLock.Finish(setting.Collection) // If provided, reuse the inproc transaction for SQLite if clientCtx := inprocgrpc.ClientContext(ctx); clientCtx != nil && b.dialect.DialectName() == "sqlite" { if externalTx := resource.TransactionFromContext(clientCtx); externalTx != nil { b.log.Info("Using SQLite transaction from client context") rsp := &resourcepb.BulkResponse{} err := b.processBulkWithTx(ctx, dbimpl.NewTx(externalTx), setting, iter, rsp) if err != nil { rsp.Error = resource.AsErrorResult(err) } return rsp } } // We may want to first write parquet, then read parquet if b.dialect.DialectName() == "sqlite" { file, err := os.CreateTemp("", "grafana-bulk-export-*.parquet") if err != nil { return &resourcepb.BulkResponse{ Error: resource.AsErrorResult(err), } } writer, err := parquet.NewParquetWriter(file) if err != nil { return &resourcepb.BulkResponse{ Error: resource.AsErrorResult(err), } } // write bulk to parquet rsp := writer.ProcessBulk(ctx, setting, iter) if rsp.Error != nil { return rsp } b.log.Info("using parquet buffer", "parquet", file) // Replace the iterator with one from parquet iter, err = parquet.NewParquetReader(file.Name(), 50) if err != nil { return &resourcepb.BulkResponse{ Error: resource.AsErrorResult(err), } } } return b.processBulk(ctx, setting, iter) } // internal bulk process func (b *backend) processBulk(ctx context.Context, setting resource.BulkSettings, iter resource.BulkRequestIterator) *resourcepb.BulkResponse { rsp := &resourcepb.BulkResponse{} err := b.db.WithTx(ctx, ReadCommitted, func(ctx context.Context, tx db.Tx) error { return b.processBulkWithTx(ctx, tx, setting, iter, rsp) }) if err != nil { rsp.Error = resource.AsErrorResult(err) } return rsp } // processBulkWithTx performs the bulk operation using the provided transaction. // This is used both when creating our own transaction and when reusing an external one. func (b *backend) processBulkWithTx(ctx context.Context, tx db.Tx, setting resource.BulkSettings, iter resource.BulkRequestIterator, rsp *resourcepb.BulkResponse) error { rollbackWithError := func(err error) error { txerr := tx.Rollback() if txerr != nil { b.log.Warn("rollback", "error", txerr) } else { b.log.Info("rollback") } return err } bulk := &bulkWroker{ ctx: ctx, tx: tx, dialect: b.dialect, logger: logging.FromContext(ctx), } // Calculate the RV based on incoming request timestamps rv := newBulkRV() summaries := make(map[string]*resourcepb.BulkResponse_Summary, len(setting.Collection)) // First clear everything in the transaction if setting.RebuildCollection { for _, key := range setting.Collection { summary, err := bulk.deleteCollection(key) if err != nil { return rollbackWithError(err) } summaries[resource.NSGR(key)] = summary rsp.Summary = append(rsp.Summary, summary) } } else { for _, key := range setting.Collection { summaries[resource.NSGR(key)] = &resourcepb.BulkResponse_Summary{ Namespace: key.Namespace, Group: key.Group, Resource: key.Resource, } } } obj := &unstructured.Unstructured{} // Write each event into the history for iter.Next() { if iter.RollbackRequested() { return rollbackWithError(nil) } req := iter.Request() if req == nil { return rollbackWithError(fmt.Errorf("missing request")) } rsp.Processed++ if req.Action == resourcepb.BulkRequest_UNKNOWN { rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ Key: req.Key, Action: req.Action, Error: "unknown action", }) continue } err := obj.UnmarshalJSON(req.Value) if err != nil { rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ Key: req.Key, Action: req.Action, Error: "unable to unmarshal json", }) continue } // Write the event to history if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{ SQLTemplate: sqltemplate.New(b.dialect), WriteEvent: resource.WriteEvent{ Key: req.Key, Type: resourcepb.WatchEvent_Type(req.Action), Value: req.Value, PreviousRV: -1, // Used for WATCH, but we want to skip watch events }, Folder: req.Folder, GUID: uuid.New().String(), ResourceVersion: rv.next(obj), }); err != nil { return rollbackWithError(fmt.Errorf("insert into resource history: %w", err)) } } // Now update the resource table from history for _, key := range setting.Collection { k := fmt.Sprintf("%s/%s/%s", key.Namespace, key.Group, key.Resource) summary := summaries[k] if summary == nil { return rollbackWithError(fmt.Errorf("missing summary key for: %s", k)) } err := bulk.syncCollection(key, summary) if err != nil { return err } if b.dialect.DialectName() == "sqlite" { nextRV, err := b.rvManager.Lock(ctx, tx, key.Group, key.Resource) if err != nil { b.log.Error("error locking RV", "error", err, "key", resource.NSGR(key)) } else { b.log.Info("successfully locked RV", "nextRV", nextRV, "key", resource.NSGR(key)) // Save the incremented RV if err := b.rvManager.SaveRV(ctx, tx, key.Group, key.Resource, nextRV); err != nil { b.log.Error("error saving RV", "error", err, "key", resource.NSGR(key)) } else { b.log.Info("successfully saved RV", "rv", nextRV, "key", resource.NSGR(key)) } } } else { // Make sure the collection RV is above our last written event _, err = b.rvManager.ExecWithRV(ctx, key, func(tx db.Tx) (string, error) { return "", nil }) if err != nil { b.log.Warn("error increasing RV", "error", err) } } // Update the last import time. This is important to trigger reindexing // of the resource for a given namespace. if err := b.updateLastImportTime(ctx, tx, key, time.Now()); err != nil { return rollbackWithError(err) } } return nil } func (b *backend) updateLastImportTime(ctx context.Context, tx db.Tx, key *resourcepb.ResourceKey, now time.Time) error { if _, err := dbutil.Exec(ctx, tx, sqlResourceLastImportTimeInsert, sqlResourceLastImportTimeInsertRequest{ SQLTemplate: sqltemplate.New(b.dialect), Namespace: key.Namespace, Group: key.Group, Resource: key.Resource, LastImportTime: now.UTC(), }); err != nil { return fmt.Errorf("insert resource last import time: %w", err) } return nil } type bulkWroker struct { ctx context.Context tx db.ContextExecer dialect sqltemplate.Dialect logger logging.Logger } // This will remove everything from the `resource` and `resource_history` table for a given namespace/group/resource func (w *bulkWroker) deleteCollection(key *resourcepb.ResourceKey) (*resourcepb.BulkResponse_Summary, error) { summary := &resourcepb.BulkResponse_Summary{ Namespace: key.Namespace, Group: key.Group, Resource: key.Resource, } // First delete history res, err := dbutil.Exec(w.ctx, w.tx, sqlResourceHistoryDelete, &sqlResourceHistoryDeleteRequest{ SQLTemplate: sqltemplate.New(w.dialect), Namespace: key.Namespace, Group: key.Group, Resource: key.Resource, }) if err != nil { return nil, err } summary.PreviousHistory, err = res.RowsAffected() if err != nil { return nil, err } // Next delete the active resource table res, err = dbutil.Exec(w.ctx, w.tx, sqlResourceDelete, &sqlResourceRequest{ SQLTemplate: sqltemplate.New(w.dialect), WriteEvent: resource.WriteEvent{ Key: key, }, }) if err != nil { return nil, err } summary.PreviousCount, err = res.RowsAffected() return summary, err } // Copy the latest value from history into the active resource table func (w *bulkWroker) syncCollection(key *resourcepb.ResourceKey, summary *resourcepb.BulkResponse_Summary) error { w.logger.Info("synchronize collection", "key", resource.NSGR(key)) _, err := dbutil.Exec(w.ctx, w.tx, sqlResourceInsertFromHistory, &sqlResourceInsertFromHistoryRequest{ SQLTemplate: sqltemplate.New(w.dialect), Key: key, }) if err != nil { return err } w.logger.Info("get stats (still in transaction)", "key", resource.NSGR(key)) rows, err := dbutil.QueryRows(w.ctx, w.tx, sqlResourceStats, &sqlStatsRequest{ SQLTemplate: sqltemplate.New(w.dialect), Namespace: key.Namespace, Group: key.Group, Resource: key.Resource, }) if err != nil { return err } if rows != nil { defer func() { _ = rows.Close() }() } if rows.Next() { row := resource.ResourceStats{} return rows.Scan(&row.Namespace, &row.Group, &row.Resource, &summary.Count, &summary.ResourceVersion) } return err }