github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvclient/kvcoord/txn_interceptor_heartbeater.go (about) 1 // Copyright 2018 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 kvcoord 12 13 import ( 14 "context" 15 "sync" 16 "time" 17 18 "github.com/cockroachdb/cockroach/pkg/roachpb" 19 "github.com/cockroachdb/cockroach/pkg/util/envutil" 20 "github.com/cockroachdb/cockroach/pkg/util/hlc" 21 "github.com/cockroachdb/cockroach/pkg/util/log" 22 "github.com/cockroachdb/cockroach/pkg/util/stop" 23 opentracing "github.com/opentracing/opentracing-go" 24 ) 25 26 // txnHeartbeatDuring1PC defines whether the txnHeartbeater should launch a 27 // heartbeat loop for 1PC transactions. The value defaults to false even though 28 // 1PC transactions leave intents around on retriable errors if the batch has 29 // been split between ranges and may be pushed when in lock wait-queues because 30 // we consider that unlikely enough so we prefer to not pay for a goroutine. 31 var txnHeartbeatFor1PC = envutil.EnvOrDefaultBool("COCKROACH_TXN_HEARTBEAT_DURING_1PC", false) 32 33 // txnHeartbeater is a txnInterceptor in charge of a transaction's heartbeat 34 // loop. Transaction coordinators heartbeat their transaction record 35 // periodically to indicate the liveness of their transaction. Other actors like 36 // concurrent transactions and GC processes observe a transaction record's last 37 // heartbeat time to learn about its disposition and to determine whether it 38 // should be considered abandoned. When a transaction is considered abandoned, 39 // other actors are free to abort it at will. As such, it is important for a 40 // transaction coordinator to heartbeat its transaction record with a 41 // periodicity well below the abandonment threshold. 42 // 43 // Transaction coordinators only need to perform heartbeats for transactions 44 // that risk running for longer than the abandonment duration. For transactions 45 // that finish well beneath this time, a heartbeat will never be sent and the 46 // EndTxn request will create and immediately finalize the transaction. However, 47 // for transactions that live long enough that they risk running into issues 48 // with other's perceiving them as abandoned, the first HeartbeatTxn request 49 // they send will create the transaction record in the PENDING state. Future 50 // heartbeats will update the transaction record to indicate progressively 51 // larger heartbeat timestamps. 52 // 53 // NOTE: there are other mechanisms by which concurrent actors could determine 54 // the liveness of transactions. One proposal is to have concurrent actors 55 // communicate directly with transaction coordinators themselves. This would 56 // avoid the need for transaction heartbeats and the PENDING transaction state 57 // entirely. Another proposal is to detect abandoned transactions and failed 58 // coordinators at an entirely different level - by maintaining a node health 59 // plane. This would function under the idea that if the node a transaction's 60 // coordinator is running on is alive then that transaction is still in-progress 61 // unless it specifies otherwise. These are both approaches we could consider in 62 // the future. 63 type txnHeartbeater struct { 64 log.AmbientContext 65 stopper *stop.Stopper 66 clock *hlc.Clock 67 metrics *TxnMetrics 68 loopInterval time.Duration 69 70 // wrapped is the next sender in the interceptor stack. 71 wrapped lockedSender 72 // gatekeeper is the sender to which heartbeat requests need to be sent. It is 73 // set to the gatekeeper interceptor, so sending directly to it will bypass 74 // all the other interceptors; heartbeats don't need them and they can only 75 // hurt - we don't want heartbeats to get sequence numbers or to check any 76 // intents. Note that the async rollbacks that this interceptor sometimes 77 // sends got through `wrapped`, not directly through `gatekeeper`. 78 gatekeeper lockedSender 79 80 // mu contains state protected by the TxnCoordSender's mutex. 81 mu struct { 82 sync.Locker 83 84 // txn is a reference to the TxnCoordSender's proto. 85 txn *roachpb.Transaction 86 87 // loopStarted indicates whether the heartbeat loop has been launched 88 // for the transaction or not. It remains true once the loop terminates. 89 loopStarted bool 90 91 // loopCancel is a function to cancel the context of the heartbeat loop. 92 // Non-nil if the heartbeat loop is currently running. 93 loopCancel func() 94 95 // finalObservedStatus is the finalized status that the heartbeat loop 96 // observed while heartbeating the transaction's record. As soon as the 97 // heartbeat loop observes a finalized status, it shuts down. 98 // 99 // If the status here is COMMITTED then the transaction definitely 100 // committed. However, if the status here is ABORTED then the 101 // transaction may or may not have been aborted. Instead, it's possible 102 // that the transaction was committed by an EndTxn request and then its 103 // record was garbage collected before the heartbeat request reached the 104 // record. The only way to distinguish this situation from a truly 105 // aborted transaction is to consider whether or not the transaction 106 // coordinator sent an EndTxn request and, if so, consider whether it 107 // succeeded or not. 108 // 109 // Because of this ambiguity, the status is not used to immediately 110 // update txn in case the heartbeat loop raced with an EndTxn request. 111 // Instead, it is used by the transaction coordinator to reject any 112 // future requests sent though it (which indicates that the heartbeat 113 // loop did not race with an EndTxn request). 114 finalObservedStatus roachpb.TransactionStatus 115 } 116 } 117 118 // init initializes the txnHeartbeater. This method exists instead of a 119 // constructor because txnHeartbeaters live in a pool in the TxnCoordSender. 120 func (h *txnHeartbeater) init( 121 ac log.AmbientContext, 122 stopper *stop.Stopper, 123 clock *hlc.Clock, 124 metrics *TxnMetrics, 125 loopInterval time.Duration, 126 gatekeeper lockedSender, 127 mu sync.Locker, 128 txn *roachpb.Transaction, 129 ) { 130 h.AmbientContext = ac 131 h.stopper = stopper 132 h.clock = clock 133 h.metrics = metrics 134 h.loopInterval = loopInterval 135 h.gatekeeper = gatekeeper 136 h.mu.Locker = mu 137 h.mu.txn = txn 138 } 139 140 // SendLocked is part of the txnInterceptor interface. 141 func (h *txnHeartbeater) SendLocked( 142 ctx context.Context, ba roachpb.BatchRequest, 143 ) (*roachpb.BatchResponse, *roachpb.Error) { 144 firstLockingIndex, pErr := firstLockingIndex(&ba) 145 if pErr != nil { 146 return nil, pErr 147 } 148 if firstLockingIndex != -1 { 149 // Set txn key based on the key of the first transactional write if not 150 // already set. If it is already set, make sure we keep the anchor key 151 // the same. 152 if len(h.mu.txn.Key) == 0 { 153 anchor := ba.Requests[firstLockingIndex].GetInner().Header().Key 154 h.mu.txn.Key = anchor 155 // Put the anchor also in the ba's copy of the txn, since this batch 156 // was prepared before we had an anchor. 157 ba.Txn.Key = anchor 158 } 159 160 // Start the heartbeat loop if it has not already started. 161 if !h.mu.loopStarted { 162 _, haveEndTxn := ba.GetArg(roachpb.EndTxn) 163 if !haveEndTxn || txnHeartbeatFor1PC { 164 if err := h.startHeartbeatLoopLocked(ctx); err != nil { 165 return nil, roachpb.NewError(err) 166 } 167 } 168 } 169 } 170 171 // Forward the batch through the wrapped lockedSender. 172 return h.wrapped.SendLocked(ctx, ba) 173 } 174 175 // setWrapped is part of the txnInterceptor interface. 176 func (h *txnHeartbeater) setWrapped(wrapped lockedSender) { 177 h.wrapped = wrapped 178 } 179 180 // populateLeafInputState is part of the txnInterceptor interface. 181 func (*txnHeartbeater) populateLeafInputState(*roachpb.LeafTxnInputState) {} 182 183 // populateLeafFinalState is part of the txnInterceptor interface. 184 func (*txnHeartbeater) populateLeafFinalState(*roachpb.LeafTxnFinalState) {} 185 186 // importLeafFinalState is part of the txnInterceptor interface. 187 func (*txnHeartbeater) importLeafFinalState(context.Context, *roachpb.LeafTxnFinalState) {} 188 189 // epochBumpedLocked is part of the txnInterceptor interface. 190 func (h *txnHeartbeater) epochBumpedLocked() {} 191 192 // createSavepointLocked is part of the txnReqInterceptor interface. 193 func (*txnHeartbeater) createSavepointLocked(context.Context, *savepoint) {} 194 195 // rollbackToSavepointLocked is part of the txnReqInterceptor interface. 196 func (*txnHeartbeater) rollbackToSavepointLocked(context.Context, savepoint) {} 197 198 // closeLocked is part of the txnInterceptor interface. 199 func (h *txnHeartbeater) closeLocked() { 200 h.cancelHeartbeatLoopLocked() 201 } 202 203 // startHeartbeatLoopLocked starts a heartbeat loop in a different goroutine. 204 func (h *txnHeartbeater) startHeartbeatLoopLocked(ctx context.Context) error { 205 if h.mu.loopStarted { 206 log.Fatal(ctx, "attempting to start a second heartbeat loop") 207 } 208 log.VEventf(ctx, 2, "coordinator spawns heartbeat loop") 209 h.mu.loopStarted = true 210 // NB: we can't do this in init() because the txn isn't populated yet then 211 // (it's zero). 212 h.AmbientContext.AddLogTag("txn-hb", h.mu.txn.Short()) 213 214 // Create a new context so that the heartbeat loop doesn't inherit the 215 // caller's cancelation. 216 // We want the loop to run in a span linked to the current one, though, so we 217 // put our span in the new context and expect RunAsyncTask to fork it 218 // immediately. 219 hbCtx := h.AnnotateCtx(context.Background()) 220 hbCtx = opentracing.ContextWithSpan(hbCtx, opentracing.SpanFromContext(ctx)) 221 hbCtx, h.mu.loopCancel = context.WithCancel(hbCtx) 222 223 return h.stopper.RunAsyncTask(hbCtx, "kv.TxnCoordSender: heartbeat loop", h.heartbeatLoop) 224 } 225 226 func (h *txnHeartbeater) cancelHeartbeatLoopLocked() { 227 // If the heartbeat loop has already started, cancel it. 228 if h.heartbeatLoopRunningLocked() { 229 h.mu.loopCancel() 230 h.mu.loopCancel = nil 231 } 232 } 233 234 func (h *txnHeartbeater) heartbeatLoopRunningLocked() bool { 235 return h.mu.loopCancel != nil 236 } 237 238 // heartbeatLoop periodically sends a HeartbeatTxn request to the transaction 239 // record, stopping in the event the transaction is aborted or committed after 240 // attempting to resolve the intents. 241 func (h *txnHeartbeater) heartbeatLoop(ctx context.Context) { 242 defer func() { 243 h.mu.Lock() 244 h.cancelHeartbeatLoopLocked() 245 h.mu.Unlock() 246 }() 247 248 var tickChan <-chan time.Time 249 { 250 ticker := time.NewTicker(h.loopInterval) 251 tickChan = ticker.C 252 defer ticker.Stop() 253 } 254 255 // Loop with ticker for periodic heartbeats. 256 for { 257 select { 258 case <-tickChan: 259 if !h.heartbeat(ctx) { 260 // The heartbeat noticed a finalized transaction, 261 // so shut down the heartbeat loop. 262 return 263 } 264 case <-ctx.Done(): 265 // Transaction finished normally. 266 return 267 case <-h.stopper.ShouldQuiesce(): 268 return 269 } 270 } 271 } 272 273 // heartbeat sends a HeartbeatTxnRequest to the txn record. 274 // Returns true if heartbeating should continue, false if the transaction is no 275 // longer Pending and so there's no point in heartbeating further. 276 func (h *txnHeartbeater) heartbeat(ctx context.Context) bool { 277 // Like with the TxnCoordSender, the locking here is peculiar. The lock is not 278 // held continuously throughout this method: we acquire the lock here and 279 // then, inside the wrapped.Send() call, the interceptor at the bottom of the 280 // stack will unlock until it receives a response. 281 h.mu.Lock() 282 defer h.mu.Unlock() 283 284 // The heartbeat loop might have raced with the cancelation of the heartbeat. 285 if ctx.Err() != nil { 286 return false 287 } 288 289 if h.mu.txn.Status != roachpb.PENDING { 290 if h.mu.txn.Status == roachpb.COMMITTED { 291 log.Fatalf(ctx, "txn committed but heartbeat loop hasn't been signaled to stop: %s", h.mu.txn) 292 } 293 // If the transaction is aborted, there's no point in heartbeating. The 294 // client needs to send a rollback. 295 return false 296 } 297 298 // Clone the txn in order to put it in the heartbeat request. 299 txn := h.mu.txn.Clone() 300 if txn.Key == nil { 301 log.Fatalf(ctx, "attempting to heartbeat txn without anchor key: %v", txn) 302 } 303 ba := roachpb.BatchRequest{} 304 ba.Txn = txn 305 ba.Add(&roachpb.HeartbeatTxnRequest{ 306 RequestHeader: roachpb.RequestHeader{ 307 Key: txn.Key, 308 }, 309 Now: h.clock.Now(), 310 }) 311 312 // Send the heartbeat request directly through the gatekeeper interceptor. 313 // See comment on h.gatekeeper for a discussion of why. 314 log.VEvent(ctx, 2, "heartbeat") 315 br, pErr := h.gatekeeper.SendLocked(ctx, ba) 316 317 // If the txn is no longer pending, ignore the result of the heartbeat 318 // and tear down the heartbeat loop. 319 if h.mu.txn.Status != roachpb.PENDING { 320 return false 321 } 322 323 var respTxn *roachpb.Transaction 324 if pErr != nil { 325 log.VEventf(ctx, 2, "heartbeat failed: %s", pErr) 326 327 // We need to be prepared here to handle the case of a 328 // TransactionAbortedError with no transaction proto in it. 329 // 330 // TODO(nvanbenschoten): Make this the only case where we get back an 331 // Aborted txn. 332 if _, ok := pErr.GetDetail().(*roachpb.TransactionAbortedError); ok { 333 // Note that it's possible that the txn actually committed but its 334 // record got GC'ed. In that case, aborting won't hurt anyone though, 335 // since all intents have already been resolved. 336 // The only thing we must ascertain is that we don't tell the client 337 // about this error - it will get either a definitive result of 338 // its commit or an ambiguous one and we have nothing to offer that 339 // provides more clarity. We do however prevent it from running more 340 // requests in case it isn't aware that the transaction is over. 341 h.abortTxnAsyncLocked(ctx) 342 h.mu.finalObservedStatus = roachpb.ABORTED 343 return false 344 } 345 346 respTxn = pErr.GetTxn() 347 } else { 348 respTxn = br.Txn 349 } 350 351 // Tear down the heartbeat loop if the response transaction is finalized. 352 if respTxn != nil && respTxn.Status.IsFinalized() { 353 switch respTxn.Status { 354 case roachpb.COMMITTED: 355 // Shut down the heartbeat loop without doing anything else. 356 // We must have raced with an EndTxn(commit=true). 357 case roachpb.ABORTED: 358 // Roll back the transaction record to clean up intents and 359 // then shut down the heartbeat loop. 360 h.abortTxnAsyncLocked(ctx) 361 } 362 h.mu.finalObservedStatus = respTxn.Status 363 return false 364 } 365 return true 366 } 367 368 // abortTxnAsyncLocked send an EndTxn(commmit=false) asynchronously. 369 // The purpose of the async cleanup is to resolve transaction intents as soon 370 // as possible when a transaction coordinator observes an ABORTED transaction. 371 func (h *txnHeartbeater) abortTxnAsyncLocked(ctx context.Context) { 372 log.VEventf(ctx, 1, "Heartbeat detected aborted txn. Cleaning up.") 373 374 // NB: We use context.Background() here because we don't want a canceled 375 // context to interrupt the aborting. 376 ctx = h.AnnotateCtx(context.Background()) 377 378 // Construct a batch with an EndTxn request. 379 txn := h.mu.txn.Clone() 380 ba := roachpb.BatchRequest{} 381 ba.Header = roachpb.Header{Txn: txn} 382 ba.Add(&roachpb.EndTxnRequest{ 383 Commit: false, 384 // Resolved intents should maintain an abort span entry to prevent 385 // concurrent requests from failing to notice the transaction was aborted. 386 Poison: true, 387 }) 388 389 log.VEventf(ctx, 2, "async abort for txn: %s", txn) 390 if err := h.stopper.RunAsyncTask( 391 ctx, "txnHeartbeater: aborting txn", func(ctx context.Context) { 392 // Send the abort request through the interceptor stack. This is 393 // important because we need the txnPipeliner to append lock spans 394 // to the EndTxn request. 395 h.mu.Lock() 396 defer h.mu.Unlock() 397 _, pErr := h.wrapped.SendLocked(ctx, ba) 398 if pErr != nil { 399 log.VErrEventf(ctx, 1, "async abort failed for %s: %s ", txn, pErr) 400 } 401 }, 402 ); err != nil { 403 log.Warningf(ctx, "%v", err) 404 } 405 } 406 407 // firstLockingIndex returns the index of the first request that acquires locks 408 // in the BatchRequest. Returns -1 if the batch has no intention to acquire 409 // locks. It also verifies that if an EndTxnRequest is included, then it is the 410 // last request in the batch. 411 func firstLockingIndex(ba *roachpb.BatchRequest) (int, *roachpb.Error) { 412 for i, ru := range ba.Requests { 413 args := ru.GetInner() 414 if i < len(ba.Requests)-1 /* if not last*/ { 415 if _, ok := args.(*roachpb.EndTxnRequest); ok { 416 return -1, roachpb.NewErrorf("%s sent as non-terminal call", args.Method()) 417 } 418 } 419 if roachpb.IsLocking(args) { 420 return i, nil 421 } 422 } 423 return -1, nil 424 }