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

RSDK-2880: Add timestamp information to the replay camera #2371

Merged
merged 38 commits into from
May 22, 2023

Conversation

dmhilly
Copy link
Member

@dmhilly dmhilly commented May 16, 2023

Summary

This PR modifies the replay camera to attach timestamps in the gRPC response if it's being called over gRPC. It also introduces an interceptor that can be used to inject metadata from the gRPC response header into the context provided by the client.

Manual Testing

I tested by configuring a robot (my Mac) with a replay camera and providing that as a sensor for SLAM. Along with the changes in the corresponding viam-cartographer PR, I confirmed that the files get written to disk with the correct timestamps correlated with the time they were captured (last week):

Screenshot 2023-05-16 at 3 31 18 PM

@github-actions
Copy link
Contributor

Warning your change may break code samples. If your change modifies any of the following functions please contact @viamrobotics/fleet-management. Thanks!

component function
base IsMoving
board GPIOPinByName
camera Properties
motor IsMoving
sensor Readings
servo Position
arm EndPosition
audio MediaProperties
gantry Lengths
gripper IsMoving
input_controller Controls
movement_sensor LinearAcceleration
pose_tracker Poses
motion GetPose
vision DetectorNames

@viambot viambot added the safe to test This pull request is marked safe to test from a trusted zone label May 16, 2023
@@ -278,6 +279,7 @@ func New(ctx context.Context, address string, logger golog.Logger, opts ...Robot
// interceptors are applied in order from first to last
rc.dialOptions = append(
rc.dialOptions,
rpc.WithUnaryClientInterceptor(rdkutils.ContextWithMetadataUnaryClientInterceptor),
Copy link
Member Author

Choose a reason for hiding this comment

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

@edaniels Do we need to register interceptors anywhere else? This is the only place that seemed necessary for this to work.

Copy link
Contributor

@jeremyrhyde jeremyrhyde left a comment

Choose a reason for hiding this comment

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

Initial pass complete with some questions for my own edification and some alternative options proposed

utils/context.go Outdated
func ContextWithMetadataUnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
var header metadata.MD
opts = append(opts, grpc.Header(&header))
invoker(ctx, method, req, reply, cc, opts...)
Copy link
Contributor

Choose a reason for hiding this comment

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

[q] The grpc.ClientConn includes an invoke function. Should we be using this instead of the passed-in invoker? Or do we need this need invoker due to the ctx with metadata?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm honestly I'm not sure. This is the pattern I've seen in most other gRPC interceptors. Probably either would work. The comment above the ClientConnInterface says:

// ClientConnInterface defines the functions clients need to perform unary and
// streaming RPCs. It is implemented by *ClientConn, and is only intended to
// be referenced by generated code.

so probably better practice to use invoker.

Copy link
Contributor

Choose a reason for hiding this comment

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

@edaniels for sanity check

Copy link
Contributor

Choose a reason for hiding this comment

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

the interceptors require you call that invoker since you don't know where in the middleware pipeline you are

Copy link
Member Author

Choose a reason for hiding this comment

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

Ahh that makes sense💡

// SubtypeName is a constant that identifies the camera resource subtype string.
SubtypeName = "camera"

// TimeRequestedMetadataKey is optional metadata in the gRPC response header that correlates
Copy link
Contributor

Choose a reason for hiding this comment

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

[opt] Move this to replaypcd. We can always import replaypcd into viam-carto and it would be good to indicate that this is currently the only model that support/uses these fields

Copy link
Member Author

Choose a reason for hiding this comment

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

I actually like having this in camera so it can be used for other camera implementations if needed. I think as long as it's well-documented, it's okay. Thoughts @edaniels ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should move this to some context utils or something. we can always move it in the future as well

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, done!

utils/context.go Outdated

const MetadataKey = "viam-metadata"

func ContextWithMetadata(ctx context.Context) (context.Context, map[string][]string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to return md here? Can it not be accessed via the ctx directly? https://pkg.go.dev/google.golang.org/grpc@v1.55.0/metadata? i.e. FromIncomingContext/FromOutgoingContext

Copy link
Member Author

Choose a reason for hiding this comment

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

The caller could call context.Value(utils.MetadataKey) instead to get the metadata. I think either design is fine; I thought this was cleaner because they don't have to check for whether or not the metadata exists (they know it does). I don't think they could call those grpc methods since at the point where this is used, the context does not have any gRPC stream in it afaik.

@dmhilly dmhilly requested review from edaniels and jeremyrhyde May 18, 2023 22:28
// SubtypeName is a constant that identifies the camera resource subtype string.
const SubtypeName = "camera"
const (
// SubtypeName is a constant that identifies the camera resource subtype string.
Copy link
Contributor

Choose a reason for hiding this comment

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

can revert this to what it was since it's one const


// TimeRequestedMetadataKey is optional metadata in the gRPC response header that correlates
// to the time right before the point cloud was captured.
TimeRequestedMetadataKey = "viam-time-requested"
Copy link
Contributor

Choose a reason for hiding this comment

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

these other consts need to be contextKey wrapped as well

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yes, I wasn't overly concerned with making this a custom type because they're keys in the metadata itself, not in the context. I don't think there's a concern that a third party would extract the viam metadata using the contextKey type but then overwrite the time requested or time received metadata keys without meaning to. The keys are also used in the gRPC header metadata, where we might be more concerned about collisions, but unfortunately gRPC requires the string type for the key so we wouldn't even be able to use a custom type if we wanted to. Lmk if you disagree with that line of reasoning though!

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh whoops that's my bad. I assumed this const block was all context keys. will add another comment to clear up confusion

@@ -0,0 +1,65 @@
// Package contextutils provides utility for adding and retrieving metadata to/from a context.
Copy link
Contributor

Choose a reason for hiding this comment

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

we'll want unit tests for this file

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah okay, I added a test for the interceptor, do you mean unit tests just for ContextWithMetadata? Showing how it works when you have metadata on the context vs not? Or is there something else you think we're missing coverage for on top of that

Copy link
Contributor

Choose a reason for hiding this comment

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

yep just simple tests of putting data in and getting it out as well as calling the Context function with a context has nothing or the wrong value paired with the context key

@@ -64,7 +64,7 @@ func TestSafetyMonitor(t *testing.T) {
}

func TestSafetyMonitorForMetadata(t *testing.T) {
stream1 := &myStream{}
stream1 := testutils.NewServerTransportStream()
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Contributor

@jeremyrhyde jeremyrhyde left a comment

Choose a reason for hiding this comment

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

Some minor nits and clean up by love all the additional testing! Signing off now so I'm not blocking you next week, feel free to grab me today or req-request a review if you want to go over any points in more depth.

}

md := ctx.Value(MetadataKey)
if mdMap, ok := md.(map[string][]string); ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should an error be returned if ContextWithMetadataUnaryClientInterceptor and no md in the map[string][]string format is available?

Copy link
Member Author

Choose a reason for hiding this comment

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

This interceptor is used for every single gRPC unary call from the gRPC client. Most of the gRPC requests will not have metadata present in the context, so we wouldn't want to error out.

testutils/rpc.go Outdated
return nil
}

// Value returns the value in the metadata map corresponding stored for the given key.
Copy link
Contributor

Choose a reason for hiding this comment

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

[opt/nit]

Suggested change
// Value returns the value in the metadata map corresponding stored for the given key.
// Value returns the value in the metadata map corresponding to a given key.

return &ServerTransportStream{}
}

// SetHeader implements grpc.ServerTransportStream.
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] might be worth explaining how this is supposed to differ from SendHeader and why in this mock up the code is the same

Copy link
Member Author

Choose a reason for hiding this comment

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

Very good question, I actually meant to follow up on this because I wasn't sure myself, and I forgot. Turns out the different between SetHeader and SendHeader is that SetHeader is supposed to merge the new metadata fields with the existing metadata fields, whereas SendHeader overwrites the metadata and is only supposed to be called once. So I think there were two things that were slightly wrong here: our implementation of SetHeader, which completely overwrites the metadata rather than merging, and the fact that in replaypcd camera I'm calling SendHeader, which afaik will overwrite any metadata already set by SetHeader and error if anyone else tries to call SendHeader or SetHeader after that point. I've changed both; thanks for calling this out! cc @edaniels for awareness of this change (Jeremy's out so can't re-request his review)

Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch

@@ -64,7 +64,7 @@ func TestSafetyMonitor(t *testing.T) {
}

func TestSafetyMonitorForMetadata(t *testing.T) {
stream1 := &myStream{}
stream1 := testutils.NewServerTransportStream()
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

@@ -28,6 +30,8 @@ import (
"go.viam.com/rdk/testutils/inject"
)

var testTime = "2000-01-01T12:00:%02dZ"
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] make a const


// Repeatedly call NextPointCloud, checking for timestamps in the gRPC header.
for i := 0; i < numPCDFiles; i++ {
serverStream := testutils.NewServerTransportStream()
Copy link
Contributor

Choose a reason for hiding this comment

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

[q] Does the metadata context need to be created every time? If not we could move lines 443-444 to 435 and call:

ctx = grpc.NewContextWithServerTransportStream(context.Background(), serverStream)

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't need to be, but I felt this more closely mimicked how this works in prod with consecutive gRPC calls using separate ServerTransportStreams and gRPC contexts. So I'm going to leave this as-is unless someone else feels strongly that this should change.

@@ -60,9 +64,17 @@ func (mDServer *mockDataServiceServer) BinaryDataByFilter(ctx context.Context, r
gz.Close()

// Construct response
timeReq, err := time.Parse(time.RFC3339, fmt.Sprintf(testTime, newFileNum))
Copy link
Contributor

Choose a reason for hiding this comment

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

[q] When should/do we need to use time.PFC3339 and time.RFC339Nano?

Copy link
Member Author

Choose a reason for hiding this comment

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

Right now, in production code paths, we're using RFC3339 for parsing the requested time ranges from the config for the replay sensor. This means that we're limiting "users" (Cloud Story) from configuring anything here with higher granularity than 1 second. This matches what we do in the Data tab and I think this is ok!

We then use RFC3339Nano whenever we're stringifying or parsing one of the timestamps correlated with the images. These have nanosecond granularity so we need to use a time format that supports that so we don't lose that information.

Here in the tests I'm using RFC3339 because I'm only changing the second of the timestamp depending on the file, so I hard-coded something in the RFC3339 format.

test.That(t, err, test.ShouldBeNil)
_, got := pcB.At(5, 5, 5)
test.That(t, got, test.ShouldBeTrue)
test.That(t, md["hello"][0], test.ShouldEqual, "world")
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you link this to the input on line 414 so they reference to the same variable?

Copy link
Contributor

@jeremyrhyde jeremyrhyde left a comment

Choose a reason for hiding this comment

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

Some minor nits and clean up by love all the additional testing! Signing off now so I'm not blocking you next week, feel free to grab me today or req-request a review if you want to go over any points in more depth.

@viambot viambot added safe to test This pull request is marked safe to test from a trusted zone and removed safe to test This pull request is marked safe to test from a trusted zone labels May 22, 2023
@viambot viambot added safe to test This pull request is marked safe to test from a trusted zone and removed safe to test This pull request is marked safe to test from a trusted zone labels May 22, 2023
@viambot viambot added safe to test This pull request is marked safe to test from a trusted zone and removed safe to test This pull request is marked safe to test from a trusted zone labels May 22, 2023
@dmhilly dmhilly requested a review from edaniels May 22, 2023 19:21
@viambot viambot added safe to test This pull request is marked safe to test from a trusted zone and removed safe to test This pull request is marked safe to test from a trusted zone labels May 22, 2023

const (
// MetadataKey is the key used to access metadata from a context with metadata.
MetadataKey = contextKey("viam-metadata")
Copy link
Contributor

Choose a reason for hiding this comment

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

the convention is to call this MetadataContextKey

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah thanks! Fixed

Copy link
Contributor

@edaniels edaniels left a comment

Choose a reason for hiding this comment

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

LGTM

@viambot viambot added safe to test This pull request is marked safe to test from a trusted zone and removed safe to test This pull request is marked safe to test from a trusted zone labels May 22, 2023
@viambot viambot added safe to test This pull request is marked safe to test from a trusted zone and removed safe to test This pull request is marked safe to test from a trusted zone labels May 22, 2023
@github-actions
Copy link
Contributor

Code Coverage

Package Line Rate Delta Health
go.viam.com/rdk/components/arm 63% 0.00%
go.viam.com/rdk/components/arm/fake 29% 0.00%
go.viam.com/rdk/components/arm/universalrobots 41% 0.00%
go.viam.com/rdk/components/arm/wrapper 21% 0.00%
go.viam.com/rdk/components/arm/xarm 23% 0.00%
go.viam.com/rdk/components/arm/yahboom 6% 0.00%
go.viam.com/rdk/components/audioinput 43% -0.47%
go.viam.com/rdk/components/base 41% 0.00%
go.viam.com/rdk/components/base/agilex 63% 0.00%
go.viam.com/rdk/components/base/fake 52% 0.00%
go.viam.com/rdk/components/base/wheeled 75% 0.00%
go.viam.com/rdk/components/board 60% 0.00%
go.viam.com/rdk/components/board/fake 40% 0.00%
go.viam.com/rdk/components/board/genericlinux 19% 0.00%
go.viam.com/rdk/components/board/numato 19% 0.00%
go.viam.com/rdk/components/board/pi 50% 0.00%
go.viam.com/rdk/components/camera 57% 0.00%
go.viam.com/rdk/components/camera/align 58% 0.00%
go.viam.com/rdk/components/camera/fake 74% 0.00%
go.viam.com/rdk/components/camera/ffmpeg 81% -1.49%
go.viam.com/rdk/components/camera/replaypcd 92% -0.11%
go.viam.com/rdk/components/camera/rtsp 45% 0.00%
go.viam.com/rdk/components/camera/transformpipeline 77% 0.00%
go.viam.com/rdk/components/camera/videosource 34% 0.00%
go.viam.com/rdk/components/encoder 57% 0.00%
go.viam.com/rdk/components/encoder/ams 64% 0.00%
go.viam.com/rdk/components/encoder/fake 83% 0.00%
go.viam.com/rdk/components/encoder/incremental 80% 0.00%
go.viam.com/rdk/components/encoder/single 86% 0.00%
go.viam.com/rdk/components/gantry 56% 0.00%
go.viam.com/rdk/components/gantry/multiaxis 87% 0.00%
go.viam.com/rdk/components/gantry/oneaxis 87% 0.00%
go.viam.com/rdk/components/generic 79% 0.00%
go.viam.com/rdk/components/gripper 69% 0.00%
go.viam.com/rdk/components/input 88% 0.00%
go.viam.com/rdk/components/input/fake 94% 0.00%
go.viam.com/rdk/components/input/gpio 85% 0.00%
go.viam.com/rdk/components/motor 71% 0.00%
go.viam.com/rdk/components/motor/dimensionengineering 66% 0.00%
go.viam.com/rdk/components/motor/dmc4000 70% 0.00%
go.viam.com/rdk/components/motor/fake 55% 0.00%
go.viam.com/rdk/components/motor/gpio 57% 0.00%
go.viam.com/rdk/components/motor/gpiostepper 52% +0.62%
go.viam.com/rdk/components/motor/tmcstepper 64% 0.00%
go.viam.com/rdk/components/motor/ulnstepper 53% 0.00%
go.viam.com/rdk/components/movementsensor 76% 0.00%
go.viam.com/rdk/components/movementsensor/adxl345 66% 0.00%
go.viam.com/rdk/components/movementsensor/cameramono 41% 0.00%
go.viam.com/rdk/components/movementsensor/gpsnmea 51% -3.50%
go.viam.com/rdk/components/movementsensor/gpsrtk 28% -5.49%
go.viam.com/rdk/components/movementsensor/mpu6050 84% 0.00%
go.viam.com/rdk/components/posetracker 71% 0.00%
go.viam.com/rdk/components/sensor 52% 0.00%
go.viam.com/rdk/components/sensor/ultrasonic 38% 0.00%
go.viam.com/rdk/components/servo 62% 0.00%
go.viam.com/rdk/components/servo/gpio 72% 0.00%
go.viam.com/rdk/config 79% 0.00%
go.viam.com/rdk/control 57% 0.00%
go.viam.com/rdk/data 77% 0.00%
go.viam.com/rdk/examples/customresources/demos/remoteserver 0% 0.00%
go.viam.com/rdk/grpc 25% 0.00%
go.viam.com/rdk/internal/cloud 100% 0.00%
go.viam.com/rdk/ml 67% 0.00%
go.viam.com/rdk/ml/inference 71% 0.00%
go.viam.com/rdk/module 76% 0.00%
go.viam.com/rdk/module/modmanager 80% 0.00%
go.viam.com/rdk/motionplan 71% +0.10%
go.viam.com/rdk/operation 82% 0.00%
go.viam.com/rdk/pointcloud 69% +0.07%
go.viam.com/rdk/protoutils 49% 0.00%
go.viam.com/rdk/referenceframe 68% 0.00%
go.viam.com/rdk/resource 76% 0.00%
go.viam.com/rdk/rimage 78% 0.00%
go.viam.com/rdk/rimage/depthadapter 94% 0.00%
go.viam.com/rdk/rimage/transform 75% 0.00%
go.viam.com/rdk/rimage/transform/cmd/extrinsic_calibration 67% 0.00%
go.viam.com/rdk/robot 86% 0.00%
go.viam.com/rdk/robot/client 82% -0.19%
go.viam.com/rdk/robot/framesystem 36% 0.00%
go.viam.com/rdk/robot/impl 82% 0.00%
go.viam.com/rdk/robot/packages 80% 0.00%
go.viam.com/rdk/robot/server 55% 0.00%
go.viam.com/rdk/robot/web 65% 0.00%
go.viam.com/rdk/robot/web/stream 87% 0.00%
go.viam.com/rdk/services/baseremotecontrol 50% 0.00%
go.viam.com/rdk/services/baseremotecontrol/builtin 82% 0.00%
go.viam.com/rdk/services/datamanager 65% 0.00%
go.viam.com/rdk/services/datamanager/builtin 89% 0.00%
go.viam.com/rdk/services/datamanager/datacapture 73% 0.00%
go.viam.com/rdk/services/datamanager/datasync 0% 0.00%
go.viam.com/rdk/services/mlmodel 83% 0.00%
go.viam.com/rdk/services/mlmodel/tflitecpu 82% 0.00%
go.viam.com/rdk/services/motion 52% 0.00%
go.viam.com/rdk/services/motion/builtin 61% 0.00%
go.viam.com/rdk/services/navigation 53% 0.00%
go.viam.com/rdk/services/sensors 81% 0.00%
go.viam.com/rdk/services/sensors/builtin 95% 0.00%
go.viam.com/rdk/services/shell 11% 0.00%
go.viam.com/rdk/services/slam 93% 0.00%
go.viam.com/rdk/services/slam/fake 90% 0.00%
go.viam.com/rdk/services/vision 35% 0.00%
go.viam.com/rdk/services/vision/colordetector 56% 0.00%
go.viam.com/rdk/services/vision/detectionstosegments 67% 0.00%
go.viam.com/rdk/services/vision/mlvision 67% 0.00%
go.viam.com/rdk/services/vision/radiusclustering 59% 0.00%
go.viam.com/rdk/session 94% 0.00%
go.viam.com/rdk/spatialmath 84% 0.00%
go.viam.com/rdk/utils 69% -0.13%
go.viam.com/rdk/utils/contextutils 38% N/A
go.viam.com/rdk/vision 26% 0.00%
go.viam.com/rdk/vision/chess 80% 0.00%
go.viam.com/rdk/vision/delaunay 87% 0.00%
go.viam.com/rdk/vision/keypoints 92% 0.00%
go.viam.com/rdk/vision/objectdetection 82% 0.00%
go.viam.com/rdk/vision/odometry 60% 0.00%
go.viam.com/rdk/vision/odometry/cmd 0% 0.00%
go.viam.com/rdk/vision/segmentation 49% 0.00%
go.viam.com/rdk/web/server 26% 0.00%
Summary 65% (21618 / 33179) -0.20%

@dmhilly
Copy link
Member Author

dmhilly commented May 22, 2023

Manually tested once again with final changes, still works. Merging

@dmhilly dmhilly merged commit 2af1019 into viamrobotics:main May 22, 2023
bazile-clyde pushed a commit to bazile-clyde/rdk that referenced this pull request Jan 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
safe to test This pull request is marked safe to test from a trusted zone
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants