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 `