Skip to content
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

move ProtoReader from lib/events to api/sessionrecording as Reader #50501

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

dustinspecker
Copy link

@dustinspecker dustinspecker commented Dec 20, 2024

This pull request does the following:

  • moves ProtoReader from lib/events to api/sessionrecording so that it may be imported by users to parse session recordings in Protobuf binary format
  • renames ProtoReader to Reader to avoid leaking implementation detail in name

This pull request is focused on moving and renaming, while #50883 makes changes to implementation and the API.

This pull request should not be merged until #50883 is also ready to be merged, since we do not want to support this API as-is.

changelog: add api/sessionrecording.Reader to support parsing and reading session recordings

@dustinspecker dustinspecker marked this pull request as ready for review December 20, 2024 18:23
@github-actions github-actions bot added audit-log Issues related to Teleports Audit Log size/sm labels Dec 20, 2024
@github-actions github-actions bot requested review from Joerger and r0mant December 20, 2024 18:23
@dustinspecker dustinspecker requested a review from zmb3 December 20, 2024 18:28
api/defaults/defaults.go Outdated Show resolved Hide resolved
api/sessionrecording/session_recording.go Show resolved Hide resolved
api/sessionrecording/session_recording.go Show resolved Hide resolved
api/sessionrecording/session_recording.go Outdated Show resolved Hide resolved
api/sessionrecording/session_recording.go Show resolved Hide resolved
Copy link
Collaborator

@zmb3 zmb3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some additional suggestions for cleanup/improvements.

We should be cautious here, and I'm fine if you want to decline to apply these, or even push them to a future PR so that this PR is more of a direct migration.

api/sessionrecording/gzip_reader.go Show resolved Hide resolved
api/sessionrecording/gzip_reader.go Show resolved Hide resolved
api/sessionrecording/session_recording.go Show resolved Hide resolved
api/sessionrecording/session_recording.go Show resolved Hide resolved
api/sessionrecording/gzip_reader.go Show resolved Hide resolved
Comment on lines +78 to +81
sizeBytes [Int64Size]byte
messageBytes [MaxProtoMessageSizeBytes]byte
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two fields are only ever used in Read, so they could be extracted to local variables instead of fields on Reader.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find. I've moved these to be local to Read.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't claim to be an expert on these things, but moving a 64kb buffer into a function-local variable seems iffy to me. It can be easy to accidentally introduce something that causes go to decide to do a heap allocation for the buffer. And even if we're not doing a heap allocation for the buffer, it still means go needs to ensure and zero 64kb of stack space each time Read() is called (potentially tens of thousands of times per second).

I think we should stick to reusing a persistent buffer for the lifetime of the reader.

Copy link
Author

@dustinspecker dustinspecker Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me and definitely not wanting to impact performance with this changed. I moved sizeBytes as well and messageBytes back to fields on the Reader struct. They are still unexported, so easy to move in the future if something changes.

api/sessionrecording/session_recording.go Show resolved Hide resolved
@dustinspecker
Copy link
Author

Some additional suggestions for cleanup/improvements.

We should be cautious here, and I'm fine if you want to decline to apply these, or even push them to a future PR so that this PR is more of a direct migration.

Normally, I would lean towards doing a direct migration. But in this case where we're making this importable then I appreciate the suggestions/improvements to make this easier for us to support consumers (plus easier to change the API now before releasing...). Thank you again for the really thorough reviews!

@zmb3
Copy link
Collaborator

zmb3 commented Jan 1, 2025

  1. We should be able to remove type bufferCloser struct { now too.
  2. It doesn't look like the Reader's Reset method is ever called, so I would remove it to keep the public API as small as possible.
  3. Lastly, maybe we can clear up what all these different readers are with some better names and comments:
  • rename reader to rawReader
  • rename inner to partReader
// Reader reads Teleport's session recordings
type Reader struct {
	// rawReader is the raw data source we read from
	rawReader io.Reader

	// partReader wraps rawReader and is limited to reading a single
	// (compressed) part from the session recording
	partReader io.Reader

	// gzipReader wraps partReader and decompresses a single part
	// from the session recording
	gzipReader *gzip.Reader

	...
}

@dustinspecker
Copy link
Author

dustinspecker commented Jan 2, 2025

  1. We should be able to remove type bufferCloser struct { now too.
  2. It doesn't look like the Reader's Reset method is ever called, so I would remove it to keep the public API as small as possible.
  3. Lastly, maybe we can clear up what all these different readers are with some better names and comments:
  • rename reader to rawReader
  • rename inner to partReader
// Reader reads Teleport's session recordings
type Reader struct {
	// rawReader is the raw data source we read from
	rawReader io.Reader

	// partReader wraps rawReader and is limited to reading a single
	// (compressed) part from the session recording
	partReader io.Reader

	// gzipReader wraps partReader and decompresses a single part
	// from the session recording
	gzipReader *gzip.Reader

	...
}
  1. Thank you - I completely overlooked this. I updated newGzipWriter to take an io.Writer. I'm still leaving our gzipWriter struct since it interacts with the writerPool.
  2. I like what you're saying and I'll look at this more. It wasn't as straight forward to remove Reset. We have a number of tests that start failing even though it looks like source code isn't using Reset anywhere. I'll investigate more, but looks like we might have test code testing test code. I want to understand the tests better before just deleting them... I was hoping just constructing a new Reader as a replacement for Reset would suffice, but it looks like some tests may be depending on lastIndex persisting across multiple provided io.Readers via Reset.
  3. Thank you for the name suggestions! Updated and added godoc.

UPDATE: Removed Reader.Reset and updated test helper function to remove duplicate events. I still want to investigate the tests better because I'm not really sure what they are testing. It's odd that tests are relying on behavior that is only implemented in test code (dropping duplicate events) across multiple session recordings.

UPDATE2: Reviewing failing integration tests.

UPDATE3: Integration test passed on re-run. I don't think I introduced any flake here, but I'm going to re-run it a few more times to be more confident.

UPDATE4: Integration tests continue to pass without any more failures. Looks like first failure might have been due to node not coming up.

lib/events/filesessions/fileasync_test.go Outdated Show resolved Hide resolved
lib/events/filesessions/fileasync_test.go Outdated Show resolved Hide resolved

outEvents = append(outEvents, out...)
// combine all uploaded parts to create the session recording content
var sessionRecordingContent bytes.Buffer
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For increased confidence, I created a new pull request right off of master to demonstrate this test change + removing Reset still passes: #50875

@dustinspecker
Copy link
Author

Hey @fspmarshall, Zac mentioned you would be a good person to take a look at these changes while he's on vacation. I'm happy to answer any questions. Ultimately, I'm wanting to use the session recording reader in another repository. Thank you in advance!

api/sessionrecording/session_recording.go Outdated Show resolved Hide resolved
Comment on lines 120 to 123
slog.Int64("skipped-bytes", p.SkippedBytes),
slog.Int64("skipped-events", p.SkippedEvents),
slog.Int64("out-of-order-events", p.OutOfOrderEvents),
slog.Int64("total-events", p.TotalEvents),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised that the linter isn't catching this(might need to file an issue upstream), but keys should be snake_case

Suggested change
slog.Int64("skipped-bytes", p.SkippedBytes),
slog.Int64("skipped-events", p.SkippedEvents),
slog.Int64("out-of-order-events", p.OutOfOrderEvents),
slog.Int64("total-events", p.TotalEvents),
slog.Int64("skipped_bytes", p.SkippedBytes),
slog.Int64("skipped_events", p.SkippedEvents),
slog.Int64("out_of_order_events", p.OutOfOrderEvents),
slog.Int64("total_events", p.TotalEvents),

Copy link
Author

@dustinspecker dustinspecker Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. That is interesting about the linter. I'll look around to see if missing some configuration/sloglint version. Otherwise, I'll file an issue upstream.

Updated to use underscores instead of hyphens.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks similar to this issue: go-simpler/sloglint#37 (go-simpler/sloglint#35)

Comment on lines +129 to +115
if r.gzipReader != nil {
return r.gzipReader.Close()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is is possible that gzipReader is assigned after the check on line 129? For instance what if Read is being executed in another goroutine?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right that is possible. I'm going to move this conversation to the other pull request that is making implementation changes: #50883

api/sessionrecording/session_recording.go Show resolved Hide resolved
api/sessionrecording/session_recording.go Outdated Show resolved Hide resolved
api/sessionrecording/session_recording.go Outdated Show resolved Hide resolved
api/sessionrecording/session_recording.go Outdated Show resolved Hide resolved
api/sessionrecording/session_recording.go Outdated Show resolved Hide resolved
api/sessionrecording/session_recording.go Outdated Show resolved Hide resolved
api/sessionrecording/session_recording.go Show resolved Hide resolved
Copy link
Contributor

@fspmarshall fspmarshall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we separate out this PR into two separate PRs? One that renames/moves but changes no meaningful code, and another for the changes?

This logic is extremely performance sensitive and extremely difficult to debug when an issue comes up. We've put a pretty significant amount of dev hours into tuning and debugging of logic related to event decoding & streaming. We really don't want to accidentally miss a potential issue because it was hidden by the diff of the move, and if a problem does sneak in it will be important to be able to isolate it in the commit log.

@dustinspecker
Copy link
Author

dustinspecker commented Jan 8, 2025

Can we separate out this PR into two separate PRs? One that renames/moves but changes no meaningful code, and another for the changes?

This logic is extremely performance sensitive and extremely difficult to debug when an issue comes up. We've put a pretty significant amount of dev hours into tuning and debugging of logic related to event decoding & streaming. We really don't want to accidentally miss a potential issue because it was hidden by the diff of the move, and if a problem does sneak in it will be important to be able to isolate it in the commit log.

Yep, I'll split this out.

EDIT: going to add a commit at the start of this branch that is a pure copy. And then the follow up commits will show removals.

@dustinspecker dustinspecker force-pushed the dustin.specker/move-proto-reader-to-api branch from 0908a30 to e9972f4 Compare January 8, 2025 19:10
This commit is broken, does not pass tests, and does not even compile.
This commit only exists to make diffs clearer as to what was changed.

The following commands all show no diff:

diff -y api/sessionrecording/sessionlog.go lib/events/sessionlog.go
diff -y api/sessionrecording/stream.go lib/events/stream.go
diff -y api/sessionrecording/stream_test.go lib/events/stream_test.go
diff -y api/sessionrecording/testdata/corrupted-session \
lib/events/testdata/corrupted-session
This commit removes unneeded code from api/sessionrecording and updates
the package name.

It removes the same code from lib/events and updates usages of this new
sessionrecording package.
This file only contains gzipReader, so rename for clarity.
The name no longer leaks the implementation. Went with Reader over
SessionRecordingReader to prevent a stutter of writing
`sessionrecording.SessionRecordingReader`.
TestReadCorruptedReading was moved to api/sessionrecording.
This is simply a constant we use to break an infinite loop.
These constants are not used by api/sessionrecording itself, so do not
export.
@dustinspecker
Copy link
Author

dustinspecker commented Jan 8, 2025

Can we separate out this PR into two separate PRs? One that renames/moves but changes no meaningful code, and another for the changes?

This logic is extremely performance sensitive and extremely difficult to debug when an issue comes up. We've put a pretty significant amount of dev hours into tuning and debugging of logic related to event decoding & streaming. We really don't want to accidentally miss a potential issue because it was hidden by the diff of the move, and if a problem does sneak in it will be important to be able to isolate it in the commit log.

I've split this into two parts now. This pull request is focused only on moving Reader and renaming. To make it easier to review, the first commit is a simply/copy paste of relevant files without any changes (it does not compile, etc.) The next commit removes unneeded functions such as the writer, etc. And follow up commits have other changes related to renaming.

#50883 has changes to implementation details and removal of APIs.

Please let me know if you have any suggestions to make this is easier to review. I know there's a lot here, so happy to spend effort making this easier on reviewers. I'm hoping the addition of the first commit with simply copy/paste makes it easier to see what was removed during the initial move.

@rosstimothy
Copy link
Contributor

Thanks for all your work on this @dustinspecker! If you plan on backporting this and #50883 I'd suggest combining both commits into a single PR on the release branches so they land at the same time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
audit-log Issues related to Teleports Audit Log size/sm
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants