github.com/pachyderm/pachyderm@v1.13.4/src/server/pkg/storage/track/postgres_tracker.go (about)

     1  package track
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"time"
     7  
     8  	"github.com/jmoiron/sqlx"
     9  	"github.com/lib/pq"
    10  )
    11  
    12  var _ Tracker = &postgresTracker{}
    13  
    14  type postgresTracker struct {
    15  	db *sqlx.DB
    16  }
    17  
    18  // NewPostgresTracker returns a
    19  func NewPostgresTracker(db *sqlx.DB) Tracker {
    20  	return &postgresTracker{db: db}
    21  }
    22  
    23  func (t *postgresTracker) CreateObject(ctx context.Context, id string, pointsTo []string, ttl time.Duration) error {
    24  	// TODO: contraints on ttl? ttl = 0 has to be interpretted as no ttl, but all other values will make it through
    25  	for _, dwn := range pointsTo {
    26  		if dwn == id {
    27  			return ErrSelfReference
    28  		}
    29  	}
    30  	return t.withTx(ctx, func(tx *sqlx.Tx) error {
    31  		var oid int
    32  		if err := func() error {
    33  			if ttl > 0 {
    34  				return tx.GetContext(ctx, &oid,
    35  					`INSERT INTO storage.tracker_objects (str_id, expires_at)
    36  				VALUES ($1, CURRENT_TIMESTAMP + $2 * interval '1 microsecond')
    37  				ON CONFLICT (str_id) DO NOTHING
    38  				RETURNING int_id
    39  				`, id, ttl.Microseconds())
    40  			}
    41  			return tx.GetContext(ctx, &oid,
    42  				`INSERT INTO storage.tracker_objects (str_id)
    43  				VALUES ($1)
    44  				ON CONFLICT (str_id) DO NOTHING
    45  				RETURNING int_id
    46  				`, id)
    47  		}(); err != nil {
    48  			if err == sql.ErrNoRows {
    49  				err = ErrObjectExists
    50  			}
    51  			return err
    52  		}
    53  		var pointsToInts []int
    54  		if err := tx.SelectContext(ctx, &pointsToInts,
    55  			`INSERT INTO storage.tracker_refs (from_id, to_id)
    56  			SELECT $1, int_id FROM storage.tracker_objects WHERE str_id = ANY($2)
    57  			RETURNING to_id`,
    58  			oid, pq.StringArray(pointsTo)); err != nil {
    59  			return err
    60  		}
    61  		if len(pointsToInts) != len(pointsTo) {
    62  			return ErrDanglingRef
    63  		}
    64  		return nil
    65  	})
    66  }
    67  
    68  func (t *postgresTracker) SetTTLPrefix(ctx context.Context, prefix string, ttl time.Duration) (time.Time, error) {
    69  	var expiresAt time.Time
    70  	err := t.db.GetContext(ctx, &expiresAt,
    71  		`UPDATE storage.tracker_objects
    72  		SET expires_at = CURRENT_TIMESTAMP + $2 * interval '1 microsecond'
    73  		WHERE str_id LIKE $1 || '%'
    74  		RETURNING expires_at`, prefix, ttl.Microseconds())
    75  	if err != nil {
    76  		return time.Time{}, err
    77  	}
    78  	return expiresAt, nil
    79  }
    80  
    81  func (t *postgresTracker) GetDownstream(ctx context.Context, id string) ([]string, error) {
    82  	dwn := []string{}
    83  	if err := t.db.SelectContext(ctx, &dwn,
    84  		`WITH target AS (
    85  			SELECT int_id FROM storage.tracker_objects WHERE str_id = $1
    86  		)
    87  		SELECT str_id
    88  		FROM storage.tracker_objects
    89  		WHERE int_id IN (
    90  			SELECT to_id FROM storage.tracker_refs WHERE from_id IN (SELECT int_id FROM target)
    91  		)`, id); err != nil {
    92  		return nil, err
    93  	}
    94  	return dwn, nil
    95  }
    96  
    97  func (t *postgresTracker) GetUpstream(ctx context.Context, id string) ([]string, error) {
    98  	ups := []string{}
    99  	if err := t.db.SelectContext(ctx, &ups,
   100  		`WITH target AS (
   101  			SELECT int_id FROM storage.tracker_objects WHERE str_id = $1
   102  		)
   103  		SELECT str_id
   104  		FROM storage.tracker_objects
   105  		WHERE int_id IN (
   106  			SELECT from_id FROM storage.tracker_refs WHERE to_id IN (SELECT int_id FROM TARGET)
   107  		)`, id); err != nil {
   108  		return nil, err
   109  	}
   110  	return ups, nil
   111  }
   112  
   113  func (t *postgresTracker) MarkTombstone(ctx context.Context, id string) error {
   114  	var tombstones []bool
   115  	if err := t.db.SelectContext(ctx, &tombstones, `	
   116  		UPDATE storage.tracker_objects
   117  		SET tombstone = (	
   118  			CASE
   119  				WHEN NOT EXISTS (
   120  					SELECT from_id FROM storage.tracker_refs
   121  					WHERE to_id IN (
   122  						SELECT int_id FROM storage.tracker_objects
   123  						WHERE str_id = $1
   124  					)
   125  				) THEN TRUE
   126  				ELSE FALSE
   127  			END
   128  		)
   129  		WHERE str_id = $1
   130  		RETURNING tombstone
   131  	`, id); err != nil {
   132  		return err
   133  	}
   134  	// if we get no results back, then it doesn't exist
   135  	if len(tombstones) == 0 {
   136  		return nil
   137  	}
   138  	// if the tombstone is not set, then it was because it would create a dangling ref
   139  	if !tombstones[0] {
   140  		return ErrDanglingRef
   141  	}
   142  	return nil
   143  }
   144  
   145  func (t *postgresTracker) FinishDelete(ctx context.Context, id string) error {
   146  	if err := t.withTx(ctx, func(tx *sqlx.Tx) error {
   147  		if _, err := tx.ExecContext(ctx, `DELETE FROM storage.tracker_refs WHERE from_id IN (
   148  			SELECT int_id FROM storage.tracker_objects WHERE str_id = $1
   149  		)
   150  		`, id); err != nil {
   151  			return err
   152  		}
   153  		var tombstone bool
   154  		if err := tx.GetContext(ctx, &tombstone,
   155  			`DELETE FROM storage.tracker_objects
   156  			WHERE str_id = $1
   157  			RETURNING tombstone
   158  			`, id); err != nil {
   159  			return err
   160  		}
   161  		if !tombstone {
   162  			return ErrNotTombstone
   163  		}
   164  		return nil
   165  	}); err != nil {
   166  		if err == sql.ErrNoRows {
   167  			return nil
   168  		}
   169  		return err
   170  	}
   171  	return nil
   172  }
   173  
   174  func (t *postgresTracker) IterateDeletable(ctx context.Context, cb func(id string) error) (retErr error) {
   175  	rows, err := t.db.QueryxContext(ctx,
   176  		`SELECT str_id FROM storage.tracker_objects
   177  		WHERE int_id NOT IN (SELECT to_id FROM storage.tracker_refs)
   178  		AND (expires_at <= CURRENT_TIMESTAMP OR tombstone)`)
   179  	if err != nil {
   180  		return err
   181  	}
   182  	defer func() {
   183  		if err := rows.Close(); retErr == nil {
   184  			retErr = err
   185  		}
   186  	}()
   187  	for rows.Next() {
   188  		var id string
   189  		if err := rows.Scan(&id); err != nil {
   190  			return err
   191  		}
   192  		if err := cb(id); err != nil {
   193  			return err
   194  		}
   195  	}
   196  	return rows.Err()
   197  }
   198  
   199  func (t *postgresTracker) withTx(ctx context.Context, cb func(tx *sqlx.Tx) error) error {
   200  	tx, err := t.db.BeginTxx(ctx, &sql.TxOptions{})
   201  	if err != nil {
   202  		return err
   203  	}
   204  	if err := cb(tx); err != nil {
   205  		tx.Rollback()
   206  		return err
   207  	}
   208  	return tx.Commit()
   209  }
   210  
   211  // SetupPostgresTracker sets up the table for the postgres tracker
   212  func SetupPostgresTracker(db *sqlx.DB) {
   213  	db.MustExec(schema)
   214  }
   215  
   216  var schema = `
   217  	CREATE TABLE IF NOT EXISTS storage.tracker_objects (
   218  		int_id BIGSERIAL PRIMARY KEY,
   219  		str_id VARCHAR(4096) UNIQUE,
   220  		tombstone BOOLEAN NOT NULL DEFAULT FALSE,
   221  		created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
   222  		expires_at TIMESTAMP
   223  	);
   224  
   225  	CREATE TABLE IF NOT EXISTS storage.tracker_refs (
   226  		from_id INT8 NOT NULL,
   227  		to_id INT8 NOT NULL,
   228  		PRIMARY KEY (from_id, to_id)
   229  	);	
   230  `