-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[CF-557] Changelog updates should wait for transaction commit #137
base: master
Are you sure you want to change the base?
Changes from all commits
b8c6173
8574a13
5c5b5d8
4221eb9
e8a2d85
209e26c
73d259a
6b11bd8
690d906
9679b40
5e21030
4dc0a35
4d4de9b
4479f75
a616e62
7f3a202
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
FROM golang:1.20-alpine | ||
FROM golang:1.20-alpine3.18 | ||
ENV SRC github.com/segmentio/ctlstore | ||
ARG VERSION | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -114,7 +114,7 @@ type supervisorCliConfig struct { | |
type ledgerHealthConfig struct { | ||
Disable bool `conf:"disable" help:"disable ledger latency health attributing (DEPRECATED: use disable-ecs-behavior instead)"` | ||
DisableECSBehavior bool `conf:"disable-ecs-behavior" help:"disable ledger latency health attributing"` | ||
MaxHealthyLatency time.Duration `conf:"max-healty-latency" help:"Max latency considered healthy"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one of the annoying aspects of this being OSS is this is a change that we can't verify doesn't break anything. Internally, I didn't see any hits for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I searched internally too -- it's a good call out that this could break something externally, but my belief is that no one is actually using this system outside of Segment, so I'm not too concerned. That being said, I could be convinced to add an "unsupported" entry that has the typo'ed configuration name and then panics. i.e., with this change if someone had the typo'ed configuration knob it would just be silently ignored AFAIU. Or even have logic to have that typo'ed configuration entry configure the "right" one and spit out a loud deprecation warning message. We don't have real releases so it's a bit unclear what the best strategy is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine just doing a breaking change. It may not be great OSS stewardship, but I have a hard time believing there's even a single other user of ctlstore outside of segment |
||
MaxHealthyLatency time.Duration `conf:"max-healthy-latency" help:"Max latency considered healthy"` | ||
AttributeName string `conf:"attribute-name" help:"The name of the attribute"` | ||
HealthyAttributeValue string `conf:"healthy-attribute-value" help:"The value of the attribute if healthy"` | ||
UnhealthyAttributeValue string `conf:"unhealth-attribute-value" help:"The value of the attribute if unhealthy"` | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -3,33 +3,101 @@ package ldbwriter | |||||||||
import ( | ||||||||||
"context" | ||||||||||
"database/sql" | ||||||||||
|
||||||||||
"github.com/segmentio/ctlstore/pkg/schema" | ||||||||||
"github.com/segmentio/ctlstore/pkg/sqlite" | ||||||||||
"github.com/segmentio/events/v2" | ||||||||||
"github.com/segmentio/stats/v4" | ||||||||||
) | ||||||||||
|
||||||||||
// CallbackWriter is an LDBWriter that delegates to another | ||||||||||
// writer and then, upon a successful write, executes N callbacks. | ||||||||||
type CallbackWriter struct { | ||||||||||
DB *sql.DB | ||||||||||
Delegate LDBWriter | ||||||||||
Callbacks []LDBWriteCallback | ||||||||||
DB *sql.DB | ||||||||||
Delegate LDBWriter | ||||||||||
Callbacks []LDBWriteCallback | ||||||||||
// Buffer between SQLite preupdate Hook and this code | ||||||||||
ChangeBuffer *sqlite.SQLChangeBuffer | ||||||||||
// Accumulated changes across multiple ApplyDMLStatement calls | ||||||||||
transactionChanges []sqlite.SQLiteWatchChange | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we consider a cap on the size of a transaction so we don't buffer too much? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting proposal, issues around that are where my head was at as I was adding the metrics. This consideration relates to the "support SoR transactions" project. We don't yet know how large SoR transactions can get, especially for those that we care about for that project. Putting some cap here with a loud complaint that we exceeded it (log + metric) and then dumping the currently buffered changes to the changelog (i.e., invoking callbacks) seems a fine behavior for now. We know that the changes would not be greater than 200 right now based on hardcoded limits elsewhere & the behavior of the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that sounds good to me There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment is not relevant to today's transactions since they are size limited but rather to the future when we are supporting SoR transactions. A sane number of statements within a transaction is smart, but by doing this we need to ensure this limit is well understood externally. Once we say we support transactions we need to be sure we really do. |
||||||||||
} | ||||||||||
|
||||||||||
func (w *CallbackWriter) inTransaction() bool { | ||||||||||
return w.transactionChanges != nil | ||||||||||
} | ||||||||||
|
||||||||||
func (w *CallbackWriter) beginTransaction(ledgerSequence schema.DMLSequence) { | ||||||||||
if len(w.transactionChanges) > 0 { | ||||||||||
// This should never happen, but just in case... | ||||||||||
stats.Add("ldb_changes_abandoned", len(w.transactionChanges)) | ||||||||||
events.Log("error: abandoned %{count}d changes from incomplete transaction, current statement's ledger sequence: %{sequence}d", | ||||||||||
len(w.transactionChanges), ledgerSequence) | ||||||||||
} | ||||||||||
w.transactionChanges = make([]sqlite.SQLiteWatchChange, 0) | ||||||||||
} | ||||||||||
|
||||||||||
// Transaction done! Return the accumulated changes including the latest ones | ||||||||||
func (w *CallbackWriter) endTransaction(changes []sqlite.SQLiteWatchChange) (transactionChanges []sqlite.SQLiteWatchChange) { | ||||||||||
w.accumulateChanges(changes) | ||||||||||
transactionChanges = w.transactionChanges | ||||||||||
w.transactionChanges = nil | ||||||||||
return | ||||||||||
} | ||||||||||
|
||||||||||
// Transaction isn't over yet, save the latest changes | ||||||||||
func (w *CallbackWriter) accumulateChanges(changes []sqlite.SQLiteWatchChange) { | ||||||||||
w.transactionChanges = append(w.transactionChanges, changes...) | ||||||||||
} | ||||||||||
|
||||||||||
// ApplyDMLStatement | ||||||||||
// | ||||||||||
// It is not obvious, but this code executes synchronously: | ||||||||||
// 1. Delegate.AppyDMLStatement executes the DML statement against the SQLite LDB. | ||||||||||
// (⚠️ WARNING: That's what the code is wired up to do today, January 2024, though the Delegate | ||||||||||
// could be doing other things since the code is so flexible.) | ||||||||||
// 2. When SQLite processes the statement it invokes our preupdate hook (see sqlite_watch.go). | ||||||||||
// 3. Our preupdate hook writes the changes to the change buffer. | ||||||||||
// 4. The code returns here, and we decide whether to process the change buffer immediately or | ||||||||||
// wait until the end of the ledger transaction. | ||||||||||
func (w *CallbackWriter) ApplyDMLStatement(ctx context.Context, statement schema.DMLStatement) error { | ||||||||||
err := w.Delegate.ApplyDMLStatement(ctx, statement) | ||||||||||
if err != nil { | ||||||||||
return err | ||||||||||
} | ||||||||||
|
||||||||||
// If beginning a transaction then start accumulating changes, don't send them out yet | ||||||||||
if statement.Statement == schema.DMLTxBeginKey { | ||||||||||
w.beginTransaction(statement.Sequence) | ||||||||||
return nil | ||||||||||
} | ||||||||||
|
||||||||||
changes := w.ChangeBuffer.Pop() | ||||||||||
|
||||||||||
// Record the responsible ledger sequence in each change so that the callback can use it | ||||||||||
for i := range changes { | ||||||||||
changes[i].LedgerSequence = statement.Sequence | ||||||||||
} | ||||||||||
|
||||||||||
var transaction bool | ||||||||||
if w.inTransaction() { | ||||||||||
transaction = true | ||||||||||
if statement.Statement == schema.DMLTxEndKey { | ||||||||||
// Transaction done, let's send what we have accumulated | ||||||||||
changes = w.endTransaction(changes) | ||||||||||
} else { | ||||||||||
// Transaction not over, continue accumulating | ||||||||||
w.accumulateChanges(changes) | ||||||||||
return nil | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
stats.Observe("ldb_changes_written", len(changes)) | ||||||||||
for _, callback := range w.Callbacks { | ||||||||||
events.Debug("Writing DML callback for %{cb}T", callback) | ||||||||||
events.Debug("Writing DML callback for %{cb}T with %{changeCount}d changes", callback, len(changes)) | ||||||||||
callback.LDBWritten(ctx, LDBWriteMetadata{ | ||||||||||
DB: w.DB, | ||||||||||
Statement: statement, | ||||||||||
Changes: changes, | ||||||||||
DB: w.DB, | ||||||||||
Statement: statement, | ||||||||||
Changes: changes, | ||||||||||
Transaction: transaction, | ||||||||||
}) | ||||||||||
} | ||||||||||
return nil | ||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really get why we inline this struct definition instead of just putitng the json tags on the ChangeLogEntry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good question -- I don't get it either.
It reminds me that I'm unsure about why we have 2 separate structs for the changelog entries. Felt like I was having to sprinkle the same fields all over the place to get it through the pipes.
ctlstore/pkg/changelog/changelog_writer.go
Lines 30 to 36 in 3652065
ctlstore/pkg/event/entry.go
Lines 3 to 11 in 3652065
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if that's a consequence of calling the root of this
pkg
vsinternal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels like there should be just one definition of this data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the json annotations too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, answers by Copilot: