github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/conn_executor_savepoints.go (about) 1 // Copyright 2020 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package sql 12 13 import ( 14 "context" 15 "strings" 16 17 "github.com/cockroachdb/cockroach/pkg/clusterversion" 18 "github.com/cockroachdb/cockroach/pkg/kv" 19 "github.com/cockroachdb/cockroach/pkg/roachpb" 20 "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" 21 "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" 22 "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" 23 "github.com/cockroachdb/cockroach/pkg/sql/sqlbase" 24 "github.com/cockroachdb/cockroach/pkg/sql/types" 25 "github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented" 26 "github.com/cockroachdb/cockroach/pkg/util/fsm" 27 "github.com/cockroachdb/cockroach/pkg/util/hlc" 28 "github.com/cockroachdb/errors" 29 ) 30 31 // commitOnReleaseSavepointName is the name of the savepoint with special 32 // release semantics: releasing this savepoint commits the underlying KV txn. 33 // This special savepoint is used to catch deferred serializability violations 34 // and is part of the client-directed transaction retries protocol. 35 const commitOnReleaseSavepointName = "cockroach_restart" 36 37 // execSavepointInOpenState runs a SAVEPOINT statement inside an open 38 // txn. 39 func (ex *connExecutor) execSavepointInOpenState( 40 ctx context.Context, s *tree.Savepoint, res RestrictedCommandResult, 41 ) (fsm.Event, fsm.EventPayload, error) { 42 savepoints := &ex.extraTxnState.savepoints 43 // Sanity check for "SAVEPOINT cockroach_restart". 44 commitOnRelease := ex.isCommitOnReleaseSavepoint(s.Name) 45 if commitOnRelease { 46 // Validate the special savepoint cockroach_restart. It cannot be nested 47 // because it has special release semantics. 48 active := ex.state.mu.txn.Active() 49 l := len(*savepoints) 50 // If we've already declared this savepoint, but we haven't done anything 51 // with the KV txn yet (or, more importantly, we haven't done an anything 52 // with the KV txn since we've rolled back to it), treat the recreation of 53 // the savepoint as a no-op instead of erroring out because this savepoint 54 // cannot be nested (even within itself). 55 // This serves to support the following pattern: 56 // SAVEPOINT cockroach_restart 57 // <foo> -> serializability failure 58 // ROLLBACK TO SAVEPOINT cockroach_restart 59 // SAVEPOINT cockroach_restart 60 // 61 // Some of our examples use this pattern, issuing the SAVEPOINT cockroach_restart 62 // inside the retry loop. 63 // 64 // Of course, this means that the following doesn't work: 65 // SAVEPOINT cockroach_restart 66 // SAVEPOINT cockroach_restart 67 // RELEASE SAVEPOINT cockroach_restart 68 // ROLLBACK TO SAVEPOINT cockroach_restart -> the savepoint no longer exists here 69 // 70 // Although it would work for any other savepoint but cockroach_restart. But 71 // that's natural given the release semantics. 72 if l == 1 && (*savepoints)[0].commitOnRelease && !active { 73 return nil, nil, nil 74 } 75 76 err := func() error { 77 if !savepoints.empty() { 78 return pgerror.Newf(pgcode.Syntax, 79 "SAVEPOINT \"%s\" cannot be nested", 80 tree.ErrNameString(commitOnReleaseSavepointName)) 81 } 82 // We want to disallow restart SAVEPOINTs to be issued after a KV 83 // transaction has started running. It is desirable to allow metadata 84 // queries against vtables to proceed before starting a SAVEPOINT for better 85 // ORM compatibility. 86 // See also https://github.com/cockroachdb/cockroach/issues/15012. 87 if ex.state.mu.txn.Active() { 88 return pgerror.Newf(pgcode.Syntax, 89 "SAVEPOINT \"%s\" needs to be the first statement in a transaction", 90 tree.ErrNameString(commitOnReleaseSavepointName)) 91 } 92 return nil 93 }() 94 if err != nil { 95 ev, payload := ex.makeErrEvent(err, s) 96 return ev, payload, nil 97 } 98 } 99 100 // We don't support savepoints in mixed-version clusters. 101 if !ex.server.cfg.Settings.Version.IsActive(ctx, clusterversion.VersionSavepoints) && !commitOnRelease { 102 err := errors.New("savepoints cannot be used until the version upgrade is finalized") 103 ev, payload := ex.makeErrEvent(err, s) 104 return ev, payload, nil 105 } 106 107 token, err := ex.state.mu.txn.CreateSavepoint(ctx) 108 if err != nil { 109 ev, payload := ex.makeErrEvent(err, s) 110 return ev, payload, nil 111 } 112 113 sp := savepoint{ 114 name: s.Name, 115 commitOnRelease: commitOnRelease, 116 kvToken: token, 117 numDDL: ex.extraTxnState.numDDL, 118 } 119 savepoints.push(sp) 120 121 return nil, nil, nil 122 } 123 124 // execRelease runs a RELEASE SAVEPOINT statement inside an open txn. 125 func (ex *connExecutor) execRelease( 126 ctx context.Context, s *tree.ReleaseSavepoint, res RestrictedCommandResult, 127 ) (fsm.Event, fsm.EventPayload) { 128 env := &ex.extraTxnState.savepoints 129 entry, idx := env.find(s.Savepoint) 130 if entry == nil { 131 ev, payload := ex.makeErrEvent( 132 pgerror.Newf(pgcode.InvalidSavepointSpecification, 133 "savepoint \"%s\" does not exist", &s.Savepoint), s) 134 return ev, payload 135 } 136 137 // Discard our savepoint and all further ones. Depending on what happens with 138 // the release below, we might add this savepoint back. 139 env.popToIdx(idx - 1) 140 141 if entry.commitOnRelease { 142 res.ResetStmtType((*tree.CommitTransaction)(nil)) 143 err := ex.commitSQLTransactionInternal(ctx, s) 144 if err == nil { 145 return eventTxnReleased{}, nil 146 } 147 // Committing the transaction failed. We'll go to state RestartWait if 148 // it's a retriable error, or to state RollbackWait otherwise. 149 if errIsRetriable(err) { 150 // Add the savepoint back. We want to allow a ROLLBACK TO SAVEPOINT 151 // cockroach_restart (that's the whole point of commitOnRelease). 152 env.push(*entry) 153 154 rc, canAutoRetry := ex.getRewindTxnCapability() 155 ev := eventRetriableErr{ 156 IsCommit: fsm.FromBool(isCommit(s)), 157 CanAutoRetry: fsm.FromBool(canAutoRetry), 158 } 159 payload := eventRetriableErrPayload{err: err, rewCap: rc} 160 return ev, payload 161 } 162 163 // Non-retriable error. The transaction might have committed (i.e. the 164 // error might be ambiguous). We can't allow a ROLLBACK TO SAVEPOINT to 165 // recover the transaction, so we're not adding the savepoint back. 166 ex.rollbackSQLTransaction(ctx) 167 ev := eventNonRetriableErr{IsCommit: fsm.FromBool(false)} 168 payload := eventNonRetriableErrPayload{err: err} 169 return ev, payload 170 } 171 172 if err := ex.state.mu.txn.ReleaseSavepoint(ctx, entry.kvToken); err != nil { 173 ev, payload := ex.makeErrEvent(err, s) 174 return ev, payload 175 } 176 177 return nil, nil 178 } 179 180 // execRollbackToSavepointInOpenState runs a ROLLBACK TO SAVEPOINT 181 // statement inside an open txn. 182 func (ex *connExecutor) execRollbackToSavepointInOpenState( 183 ctx context.Context, s *tree.RollbackToSavepoint, res RestrictedCommandResult, 184 ) (fsm.Event, fsm.EventPayload) { 185 entry, idx := ex.extraTxnState.savepoints.find(s.Savepoint) 186 if entry == nil { 187 ev, payload := ex.makeErrEvent(pgerror.Newf(pgcode.InvalidSavepointSpecification, 188 "savepoint \"%s\" does not exist", &s.Savepoint), s) 189 return ev, payload 190 } 191 192 if ev, payload, ok := ex.checkRollbackValidity(ctx, s, entry); !ok { 193 return ev, payload 194 } 195 196 // Special case for mixed-cluster versions, where regular savepoints 197 // are not yet enabled but we still support cockroach_restart. In 198 // that case, we can't process ROLLBACK TO SAVEPOINT 199 // cockroach_restart using ignored seqnum lists so we need to 200 // restart the txn the "old way". 201 // TODO(knz): Remove this check in v20.2 and only keep the 'else' 202 // clause, which is the generic rollback code. 203 if entry.kvToken.Initial() && 204 !ex.server.cfg.Settings.Version.IsActive(ctx, clusterversion.VersionSavepoints) { 205 // Bump the epoch manually. 206 ex.state.mu.txn.ManualRestart(ctx, hlc.Timestamp{}) 207 } else { 208 if err := ex.state.mu.txn.RollbackToSavepoint(ctx, entry.kvToken); err != nil { 209 ev, payload := ex.makeErrEvent(err, s) 210 return ev, payload 211 } 212 } 213 214 ex.extraTxnState.savepoints.popToIdx(idx) 215 216 if entry.kvToken.Initial() { 217 return eventTxnRestart{}, nil 218 } 219 // No event is necessary; there's nothing for the state machine to do. 220 return nil, nil 221 } 222 223 // checkRollbackValidity verifies that a ROLLBACK TO SAVEPOINT 224 // statement should be allowed in the current txn state. 225 // It returns ok == false if the operation should be prevented 226 // from proceeding, in which case it also populates the event 227 // and payload with a suitable user error. 228 func (ex *connExecutor) checkRollbackValidity( 229 ctx context.Context, s *tree.RollbackToSavepoint, entry *savepoint, 230 ) (ev fsm.Event, payload fsm.EventPayload, ok bool) { 231 if ex.extraTxnState.numDDL <= entry.numDDL { 232 // No DDL; all the checks below only care about txns containing 233 // DDL, so we don't have anything else to do here. 234 return ev, payload, true 235 } 236 237 if !entry.kvToken.Initial() { 238 // We don't yet support rolling back a regular savepoint over 239 // DDL. Instead of creating an inconsistent txn or schema state, 240 // prefer to tell the users we don't know how to proceed 241 // yet. Initial savepoints are a special case - we can always 242 // rollback to them because we can reset all the schema change 243 // state. 244 ev, payload = ex.makeErrEvent(unimplemented.NewWithIssueDetail(10735, "rollback-after-ddl", 245 "ROLLBACK TO SAVEPOINT not yet supported after DDL statements"), s) 246 return ev, payload, false 247 } 248 249 if ex.state.mu.txn.UserPriority() == roachpb.MaxUserPriority { 250 // Because we use the same priority (MaxUserPriority) for SET 251 // TRANSACTION PRIORITY HIGH and lease acquisitions, we'd get a 252 // deadlock if we let DDL proceed at high priority. 253 // See https://github.com/cockroachdb/cockroach/issues/46414 254 // for details. 255 // 256 // Note: this check must remain even when regular savepoints are 257 // taught to roll back over DDL (that's the other check in 258 // execSavepointInOpenState), until #46414 gets solved. 259 ev, payload = ex.makeErrEvent(unimplemented.NewWithIssue(46414, 260 "cannot use ROLLBACK TO SAVEPOINT in a HIGH PRIORITY transaction containing DDL"), s) 261 return ev, payload, false 262 } 263 264 return ev, payload, true 265 } 266 267 func (ex *connExecutor) execRollbackToSavepointInAbortedState( 268 ctx context.Context, s *tree.RollbackToSavepoint, 269 ) (fsm.Event, fsm.EventPayload) { 270 makeErr := func(err error) (fsm.Event, fsm.EventPayload) { 271 ev := eventNonRetriableErr{IsCommit: fsm.False} 272 payload := eventNonRetriableErrPayload{ 273 err: err, 274 } 275 return ev, payload 276 } 277 278 entry, idx := ex.extraTxnState.savepoints.find(s.Savepoint) 279 if entry == nil { 280 return makeErr(pgerror.Newf(pgcode.InvalidSavepointSpecification, 281 "savepoint \"%s\" does not exist", tree.ErrString(&s.Savepoint))) 282 } 283 284 if ev, payload, ok := ex.checkRollbackValidity(ctx, s, entry); !ok { 285 return ev, payload 286 } 287 288 ex.extraTxnState.savepoints.popToIdx(idx) 289 290 // Special case for mixed-cluster versions, where regular savepoints 291 // are not yet enabled but we still support cockroach_restart. In 292 // that case, we can't process ROLLBACK TO SAVEPOINT 293 // cockroach_restart using ignored seqnum lists so we need to 294 // restart the txn the "old way". 295 // TODO(knz): Remove this check in v20.2 and only keep the 'else' 296 // clause, which is the generic rollback code. 297 if entry.kvToken.Initial() && 298 !ex.server.cfg.Settings.Version.IsActive(ctx, clusterversion.VersionSavepoints) { 299 // Bump the epoch manually. 300 ex.state.mu.txn.ManualRestart(ctx, hlc.Timestamp{}) 301 } else { 302 if err := ex.state.mu.txn.RollbackToSavepoint(ctx, entry.kvToken); err != nil { 303 return ex.makeErrEvent(err, s) 304 } 305 } 306 307 if entry.kvToken.Initial() { 308 return eventTxnRestart{}, nil 309 } 310 return eventSavepointRollback{}, nil 311 } 312 313 // isCommitOnReleaseSavepoint returns true if the savepoint name implies special 314 // release semantics: releasing it commits the underlying KV txn. 315 func (ex *connExecutor) isCommitOnReleaseSavepoint(savepoint tree.Name) bool { 316 if ex.sessionData.ForceSavepointRestart { 317 // The session setting force_savepoint_restart implies that all 318 // uses of the SAVEPOINT statement are targeting restarts. 319 return true 320 } 321 return strings.HasPrefix(string(savepoint), commitOnReleaseSavepointName) 322 } 323 324 // savepoint represents a SQL savepoint - a snapshot of the current 325 // transaction's state at a previous point in time. 326 // 327 // Savepoints' behavior on RELEASE differs based on commitOnRelease, and their 328 // behavior on ROLLBACK after retriable errors differs based on 329 // kvToken.Initial(). 330 type savepoint struct { 331 name tree.Name 332 333 // commitOnRelease is set if the special syntax "SAVEPOINT cockroach_restart" 334 // was used. Such a savepoint is special in that a RELEASE actually commits 335 // the transaction - giving the client a change to find out about any 336 // retriable error and issue another "ROLLBACK TO SAVEPOINT cockroach_restart" 337 // afterwards. Regular savepoints (even top-level savepoints) cannot commit 338 // the transaction on RELEASE. 339 // 340 // Only an `initial` savepoint can have this set (see 341 // client.SavepointToken.Initial()). 342 commitOnRelease bool 343 344 kvToken kv.SavepointToken 345 346 // The number of DDL statements that had been executed in the transaction (at 347 // the time the savepoint was created). We refuse to roll back a savepoint if 348 // more DDL statements were executed since the savepoint's creation. 349 // TODO(knz): support partial DDL cancellation in pending txns. 350 numDDL int 351 } 352 353 type savepointStack []savepoint 354 355 func (stack savepointStack) empty() bool { return len(stack) == 0 } 356 357 func (stack *savepointStack) clear() { *stack = (*stack)[:0] } 358 359 func (stack *savepointStack) push(s savepoint) { 360 *stack = append(*stack, s) 361 } 362 363 // find finds the most recent savepoint with the given name. 364 // 365 // The returned savepoint can be modified (rolling back modifies the kvToken). 366 // Callers shouldn't maintain references to the returned savepoint, as 367 // references can be invalidated by further operations on the savepoints. 368 func (stack savepointStack) find(sn tree.Name) (*savepoint, int) { 369 for i := len(stack) - 1; i >= 0; i-- { 370 if stack[i].name == sn { 371 return &stack[i], i 372 } 373 } 374 return nil, -1 375 } 376 377 // popToIdx pops (discards) all the savepoints at higher indexes. 378 func (stack *savepointStack) popToIdx(idx int) { 379 *stack = (*stack)[:idx+1] 380 } 381 382 func (stack savepointStack) clone() savepointStack { 383 if len(stack) == 0 { 384 // Avoid allocating a slice. 385 return nil 386 } 387 cpy := make(savepointStack, len(stack)) 388 copy(cpy, stack) 389 return cpy 390 } 391 392 // runShowSavepointState executes a SHOW SAVEPOINT STATUS statement. 393 // 394 // If an error is returned, the connection needs to stop processing queries. 395 func (ex *connExecutor) runShowSavepointState( 396 ctx context.Context, res RestrictedCommandResult, 397 ) error { 398 res.SetColumns(ctx, sqlbase.ResultColumns{ 399 {Name: "savepoint_name", Typ: types.String}, 400 {Name: "is_initial_savepoint", Typ: types.Bool}, 401 }) 402 403 for _, entry := range ex.extraTxnState.savepoints { 404 if err := res.AddRow(ctx, tree.Datums{ 405 tree.NewDString(string(entry.name)), 406 tree.MakeDBool(tree.DBool(entry.kvToken.Initial())), 407 }); err != nil { 408 return err 409 } 410 } 411 return nil 412 }