Thinking about context cancellation #250
Replies: 3 comments 10 replies
-
I made some formatting tweaks to your post, hope you don't mind. The code had already changed since the previous discussion, which fixed some of your concerns. The statement is now only made active once the context cancels: Lines 371 to 380 in 9e4258b And then it's reset as soon as a new, not-yet-cancelled context is set: Lines 343 to 369 in 9e4258b So the statement is active only while the interruption lasts. You do make a good point that cancelling I disagree with renaming stuff. Contexts are about more than cancellation, and I don't want to give anyone the impression they can use them for anything else. Particularly, because I reserve the right to change the interrupt context momentarily (as I do/did in various places). |
Beta Was this translation helpful? Give feedback.
-
I'll think about it a bit more, but in a first pass it looks quite good. Much simpler. Here are two things I'm trying to work through. There's a potential race of sorts. Scenario 1: User calls Scenario 2: Context is cancelled, user calls In both scenarios the user gets the same error code, but one was from SQLite and one was from ncruces. In the first scenario there may have been some automatic cleanup (e.g., here), in the second there wasn't. Probably doesn't matter, probably OK, query should eventually get rolled back, statement should eventually get reset, but if there is a difference it would be one of those "sometimes something weird happens" things, all but impossible to understand. Your convenience |
Beta Was this translation helpful? Give feedback.
-
I extended the test to add a call to func TestAutocommit(t *testing.T) {
db := newPopulatedDB(t, *defaultMaxReadConnections, *defaultMaxWriteConnections, *defaultPosts, *defaultPostParagraphs, *defaultComments, *defaultCommentParagraphs)
defer db.Close()
conn, err := db.writePool.Get(t.Context())
noErr(t, err)
defer db.writePool.Put(conn)
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
_ctx := conn.SetInterrupt(ctx)
defer conn.SetInterrupt(_ctx)
s, _, err := conn.Prepare(`
DELETE FROM comments
WHERE id <= 10
RETURNING id;`)
noErr(t, err)
defer s.Close()
t.Log("Before Step")
t.Log("Autocommit", conn.GetAutocommit())
t.Log("TxnState", conn.TxnState(""))
for s.Step() {
t.Log("During Step")
t.Log("Autocommit", conn.GetAutocommit())
t.Log("TxnState", conn.TxnState(""))
cancel()
}
t.Log("After Step")
t.Log("Autocommit", conn.GetAutocommit())
t.Log("TxnState", conn.TxnState(""))
}
|
Beta Was this translation helpful? Give feedback.
-
You mentioned that vacuum won’t work in the presence of an active statement (here).
That’s not the only example.
Collations can’t be modified or deleted (here).
Functions can’t be modified or deleted (here).
After a
SQLITE3_SCHEMA
errorsqlite3_changes()
is set to zero if there's more than one active statements (here).This pager snapshot check only happens if there are no active statements.
There might be others, but that’s already too many!
An always-active statement has visible side-effects.
Given that Prepare, Step and Exec all re-apply interrupts it’s not clear to me that you need an always active statement anyway. I'm not sure it's worth puzzling that out, though, because I think there’s another issue: contexts have dynamic extent while interrupts affect all active statements in a connection.
Consider this calling pattern:
Or this one:
In both cases cancelling
ctxInner
will interrupt both the inner and outer queries.I'm not sure that’s what a programmer reading the code would expect?
If you don’t think the second issue is a real issue, I see two options.
Both abandon the use of an always-active statement.
The rest of the code stays as it is. SQLite will be re-interrupted at all of the main entry points. It might take a little longer for everything to stop, but stop it will.
Have checkInterrupt return context.Err() (after interrupting SQLite), and have the routines that call it early exit in the normal Go way, returning that error when it isn't nil — i.e. without calling in to SQLite. There are pros and cons to that approach.
If you think the second issue is a real issue, I see two options.
Both abandon the use of an always-active statement and interrupt.
Like (2) early exit, returning context.Err() if it isn't nil — but without the intervening call to interrupt.
Let the progress callback (and busy handler) eventually “interrupt” long running queries if the context is cancelled, and don't intervene anywhere else.
I think both of those options will respect the dynamic extent of contexts; only the queries that correspond to a cancelled context will (eventually) be “interrupted.”
Personally I think the second issue is a real issue based on my understanding of your underlying goal: predictability. By which I mean: the ability to build a correct mental model, and anticipate its implications.
If you go with (3) or (4) I’d suggest renaming (deprecating) SetInterrupt() in favour of SetContext(); renaming (deprecating) GetInterrupt() in favour of Context(); and renaming interrupt to ctx. That would just make things easier for Go programmers to understand.
As I said in the other discussion, this isn't a performance issue; I don't think these changes would have a major impact on performance.
Beta Was this translation helpful? Give feedback.
All reactions