@@ -133,6 +133,49 @@ func changefeedTypeCheck(
133133 return true , withSinkHeader , nil
134134}
135135
136+ func maybeShowCursorAgeWarning (
137+ ctx context.Context , p sql.PlanHookState , opts changefeedbase.StatementOptions ,
138+ ) error {
139+ st , err := opts .GetInitialScanType ()
140+ if err != nil {
141+ return err
142+ }
143+
144+ if ! opts .HasStartCursor () || st == changefeedbase .OnlyInitialScan {
145+ return nil
146+ }
147+ statementTS := p .ExtendedEvalContext ().GetStmtTimestamp ().UnixNano ()
148+ cursorTS , err := evalCursor (ctx , p , hlc.Timestamp {WallTime : statementTS }, opts .GetCursor ())
149+ if err != nil {
150+ return err
151+ }
152+
153+ warningAge := int64 (5 * time .Hour )
154+ cursorAge := func () int64 {
155+ knobs , _ := p .ExecCfg ().DistSQLSrv .TestingKnobs .Changefeed .(* TestingKnobs )
156+ if knobs != nil && knobs .OverrideCursorAge != nil {
157+ return knobs .OverrideCursorAge ()
158+ }
159+ return statementTS - cursorTS .WallTime
160+ }()
161+
162+ if cursorAge > warningAge {
163+ err = p .SendClientNotice (ctx ,
164+ pgnotice .Newf (
165+ `the provided cursor is %d hours old; ` +
166+ `older cursors can result in increased changefeed latency` ,
167+ cursorAge / int64 (time .Hour ),
168+ ),
169+ true ,
170+ )
171+ if err != nil {
172+ return err
173+ }
174+ }
175+
176+ return nil
177+ }
178+
136179// changefeedPlanHook implements sql.planHookFn.
137180func changefeedPlanHook (
138181 ctx context.Context , stmt tree.Statement , p sql.PlanHookState ,
@@ -229,6 +272,9 @@ func changefeedPlanHook(
229272
230273 telemetry .Count (`changefeed.create.core` )
231274 logChangefeedCreateTelemetry (ctx , jr , changefeedStmt .Select != nil )
275+ if err := maybeShowCursorAgeWarning (ctx , p , opts ); err != nil {
276+ return err
277+ }
232278
233279 err := coreChangefeed (ctx , p , details , description , progress , resultsCh )
234280 // TODO(yevgeniy): This seems wrong -- core changefeeds always terminate
@@ -314,6 +360,10 @@ func changefeedPlanHook(
314360 return err
315361 }
316362
363+ if err := maybeShowCursorAgeWarning (ctx , p , opts ); err != nil {
364+ return err
365+ }
366+
317367 logChangefeedCreateTelemetry (ctx , jr , changefeedStmt .Select != nil )
318368
319369 select {
@@ -388,6 +438,22 @@ func coreChangefeed(
388438 }
389439}
390440
441+ func evalCursor (
442+ ctx context.Context , p sql.PlanHookState , statementTime hlc.Timestamp , timeString string ,
443+ ) (hlc.Timestamp , error ) {
444+ if knobs , ok := p .ExecCfg ().DistSQLSrv .TestingKnobs .Changefeed .(* TestingKnobs ); ok {
445+ if knobs != nil && knobs .OverrideCursor != nil {
446+ timeString = knobs .OverrideCursor (& statementTime )
447+ }
448+ }
449+ asOfClause := tree.AsOfClause {Expr : tree .NewStrVal (timeString )}
450+ asOf , err := p .EvalAsOfTimestamp (ctx , asOfClause )
451+ if err != nil {
452+ return hlc.Timestamp {}, err
453+ }
454+ return asOf .Timestamp , nil
455+ }
456+
391457func createChangefeedJobRecord (
392458 ctx context.Context ,
393459 p sql.PlanHookState ,
@@ -408,22 +474,10 @@ func createChangefeedJobRecord(
408474 WallTime : p .ExtendedEvalContext ().GetStmtTimestamp ().UnixNano (),
409475 }
410476 var initialHighWater hlc.Timestamp
411- evalTimestamp := func (s string ) (hlc.Timestamp , error ) {
412- if knobs , ok := p .ExecCfg ().DistSQLSrv .TestingKnobs .Changefeed .(* TestingKnobs ); ok {
413- if knobs != nil && knobs .OverrideCursor != nil {
414- s = knobs .OverrideCursor (& statementTime )
415- }
416- }
417- asOfClause := tree.AsOfClause {Expr : tree .NewStrVal (s )}
418- asOf , err := p .EvalAsOfTimestamp (ctx , asOfClause )
419- if err != nil {
420- return hlc.Timestamp {}, err
421- }
422- return asOf .Timestamp , nil
423- }
477+
424478 if opts .HasStartCursor () {
425479 var err error
426- initialHighWater , err = evalTimestamp ( opts .GetCursor ())
480+ initialHighWater , err = evalCursor ( ctx , p , statementTime , opts .GetCursor ())
427481 if err != nil {
428482 return nil , err
429483 }
0 commit comments