Search code examples
gogrpcgolandgrpc-goproto3

rpc error: code = Unavailable desc = error reading from server: EOF resulting in a panic runtime error: invalid memory address error on goland


I am new Stack Overflow and this is my first question so I'm very open and happy to make any improvements to it :) I'm having an issue when I run a test method to unlike an artwork. I have a method that enables a user to like a specific artwork which is categorised by their own separate artwork uuid's, and the method works perfectly. I have a console.proto file where I specify the necessary request and response messages for both the LikeArtwork and UnlikeArtwork methods, and have created the methods themselves in a methods.go file. I have other methods that I am testing and they all seem to work perfectly except for the UnlikeArtwork method.

I have a followUser and UnfollowUser method, which both work in exactly the same way, except in the UnfollowUser method, a user is just removed from the "followers" slice/array that contains all of the followers. The concept is exactly the same for the likeArtwork and UnlikeArtwork methods where users are either added or removed from the "likes" slice/array, which is why I am so confused as to why it is causing my User's server to crash with the error:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x58 pc=0x104d0b844]

My console.proto is defined as follows:

service Service {
  // Registers a user on the hockney platform/ This entails creating the user
  // using the CreateUser method and uploading their profile picture using the
  // UploadProfilePicture method/ Down the line, this could also entail the
  // creation of a custodian wallet for users as well/
  rpc RegisterUser (RegisterUserRequest) returns (RegisterUserResponse) {}
  // Follow user takes the user of the current session and updates their details
  // and that of the followee to reflect the following
  rpc FollowUser (FollowUserRequest) returns (FollowUserResponse) {}
  // Like an artwork
  rpc LikeArtwork (LikeArtworkRequest) returns (LikeArtworkResponse) {}
  // Unfollow a user
  rpc UnfollowUser (UnfollowUserRequest) returns (UnfollowUserResponse) {}
  // Unlike an artwork
  rpc UnlikeArtwork (UnlikeArtworkRequest) returns (UnlikeArtworkResponse) {}
  // New UnLike an artwork method
  rpc UnLikeArtwork (UnLikeArtworkRequest) returns (UnLikeArtworkResponse) {}
}

and,

// Request message for the FollowUser method
message FollowUserRequest {
  // The user of the current session
  // Format: users/{username}
  string current_user = 1;
  // The user to follow
  // Format: users/{username}
  string user_to_follow = 2;
}
// Response message for the FollowUser method
// Reserved for future use...
message FollowUserResponse {}

// Request message for LikeArtwork
message LikeArtworkRequest {
  // The user that likes
  // Format: users/{username}
  string user = 1;
  // The artwork that has been liked
  // Format: artworks/{uuid}
  string artwork = 2;
}

// Response message for LikeArtwork
message LikeArtworkResponse {}

// Request message for the UnfollowUser method
message UnfollowUserRequest {
  // The user of the current session
  // Format: users/{username}
  string current_user = 1;
  // The user to unfollow
  // Format: users/{username}
  string user_to_unfollow = 2;
}

// Response message for UnfollowUser method
message UnfollowUserResponse {}

// Request message for the UnlikeArtwork method
message UnlikeArtworkRequest {
  // The user that unlikes
  // Format: users/{username}
  string user = 1;
  // The artwork that has been unliked
  // Format: artworks/{uuid}
  string artwork_to_unlike = 2;
}

// Response message for the UnlikeArtwork method
message UnlikeArtworkResponse {}

The method I wrote to unlike an artwork works in exactly the same way as the LikeArtwork method, however, a user is removed from the "likes" slice. This occurs when currentUser.Likes = append(...)... . I don't think thats where the issue lies, since running this code through the Goland debugger, the error seems to occur before it gets to this part of the function. The function for this method is shown below:

func (s *myService) UnlikeArtwork(ctx context.Context, req *pb.UnlikeArtworkRequest) (*pb.UnlikeArtworkResponse, error) {

    currentUser, err := clients.Users.GetUser(ctx, &pbUsers.GetUserRequest{
        Name:     req.GetUser(),
        ReadMask: &fieldmaskpb.FieldMask{Paths: []string{"likes"}},
    })
    if err != nil {
        return nil, err
    }

    // Set new likes
    //currentUser.Following = append(currentUser.Following[:sort.StringSlice(currentUser.Following).Search(req.GetUserToUnfollow())], currentUser.Following[sort.StringSlice(currentUser.Following).Search(req.GetUserToUnfollow())+1:]...)
    currentUser.Likes = append(currentUser.Likes[:sort.StringSlice(currentUser.Likes).Search(req.GetUser())], currentUser.Likes[sort.StringSlice(currentUser.Likes).Search(req.GetUser())+1:]...)

    // Update the current_user
    _, err = clients.Users.UpdateUser(ctx, &pbUsers.UpdateUserRequest{
        User:       currentUser,
        UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"likes"}},
    })
    if err != nil {
        return nil, err
    }

    currentArtwork, err := clients.Artworks.GetArtwork(ctx, &pbArtworks.GetArtworkRequest{
        Name:     req.GetArtworkToUnlike(),
        ReadMask: &fieldmaskpb.FieldMask{Paths: []string{"likes"}},
    })
    if err != nil {
        return nil, err
    }

    // Set the likes
    //userToUnfollow.Followers = append(userToUnfollow.Followers[:sort.StringSlice(userToUnfollow.Followers).Search(req.GetCurrentUser())], userToUnfollow.Followers[sort.StringSlice(userToUnfollow.Followers).Search(req.GetCurrentUser())+1:]...)
    currentArtwork.Likes = append(currentArtwork.Likes[:sort.StringSlice(currentArtwork.Likes).Search(req.GetArtworkToUnlike())], currentArtwork.Likes[sort.StringSlice(currentArtwork.Likes).Search(req.GetArtworkToUnlike())+1:]...)

    // Update the current artwork
    _, err = clients.Artworks.UpdateArtwork(ctx, &pbArtworks.UpdateArtworkRequest{
        Artwork:    currentArtwork,
        UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"likes"}},
    })
    if err != nil {
        return nil, err
    }

    return &pb.UnlikeArtworkResponse{}, nil
}

The test was then run in a methods_test.go file where the inputs to the function and the function itself was simulated.

func TestMyService_UnlikeArtwork(t *testing.T) {
    req := pbConsole.UnlikeArtworkRequest{
        User:            "PurpleRaine",
        ArtworkToUnlike: "artworks/0cca6063-7b6f-464a-ac88-dff8679a3905",
    }

    // Run a method
    res, err := client.UnlikeArtwork(context.Background(), &req)
    if err != nil {
        t.Error(err)
    }

    log.Println(logging.Entry{Message: res.String()})
}

The output of the test is:

methods_test.go:111: rpc error: code = Unavailable desc = error reading from server: EOF
methods_test.go:114: {"message":"\u003cnil\u003e","severity":"INFO"}
--- FAIL: TestMyService_UnLikeArtwork (0.01s)

FAIL

and this happens along with the server crash error listed at the beginning of this question.

Additional Information: My user's server is configured as follows:

package main

import (
    "context"
    "fmt"
    pb "github.com/jaebrownn/hockney/protobuf/go/hock/ap/resources/users/v1"
    "google.golang.org/grpc"
    "hock.ap.resources.users.v1/internal/logging"
    "hock.ap.resources.users.v1/internal/methods"
    "log"
    "net"
    "os"
)

// clients is a global clients, initialized once per cloud run instance.
var ()

func init() {
    // Disable log prefixes such as the default timestamp.
    // Prefix text prevents the message from being parsed as JSON.
    // A timestamp is added when shipping logs to Cloud Logging.
    log.SetFlags(0)

    // TODO: implement when needed
    // Ensure that required envs exist.
    //if os.Getenv("ENV") == "" {
    //  log.Fatal("ENV env not set.")
    //}
}

func main() {
    log.Println(&logging.Entry{Message: "starting server...", Severity: logging.NOTICE})

    port := os.Getenv("USERS_PORT")
    if port == "" {
        port = "8080"
        log.Println(&logging.Entry{Message: "Defaulting to port " + port, Severity: logging.WARNING})
    }

    listener, err := net.Listen("tcp", ":"+port)
    if err != nil {
        log.Fatalf("net.Listen: %v", err)
    }

    grpcServer := grpc.NewServer(grpc.UnaryInterceptor(serverInterceptor))
    pb.RegisterUsersServiceServer(grpcServer, &methods.MyService{})

    if err = grpcServer.Serve(listener); err != nil {
        log.Fatal(err)
    }
}

// serverInterceptor is an example of a Server Interceptor which could be used to 'inject'
// for example logs and/or tracing details to incoming server requests.
// Add this method to your grpc server connection, for example
// grpcServer := grpc.NewServer(grpc.UnaryInterceptor(serverInterceptor))
//
//  pb.RegisterServiceServer(grpcServer, &myService{})
func serverInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

    // Calls the handler
    h, err := handler(ctx, req)
    if err != nil {
        log.Println(&logging.Entry{Message: fmt.Sprintf("%v", req), Severity: logging.DEBUG, Ctx: ctx})
        log.Println(&logging.Entry{Message: err.Error(), Severity: logging.WARNING, Ctx: ctx})
    }

    _ = info
    return h, err
}

Edited: Here is the full stack trace when the crash occurs:

goroutine 54 [running]:
cloud.google.com/go/firestore.(*DocumentSnapshot).DataTo(0x0?, {0x104e28820?, 0x140000d02c0?})
        /Users/michaelaltshuler/go/pkg/mod/cloud.google.com/go/[email protected]/document.go:112 +0x24
hock.ap.resources.users.v1/internal/methods.(*MyService).GetUser(0x1400011f9a8?, {0x104e4ea60, 0x14000102750}, 0x14?)
        /Users/michaelaltshuler/GolandProjects/hockney/products/hock/ap/resources/users/v1/internal/methods/methods.go:59 +0x84
github.com/jaebrownn/hockney/protobuf/go/hock/ap/resources/users/v1._UsersService_GetUser_Handler.func1({0x104e4ea60, 0x14000102750}, {0x104dd2d40?, 0x14000046340})
        /Users/michaelaltshuler/GolandProjects/hockney/protobuf/go/hock/ap/resources/users/v1/users_grpc.pb.go:212 +0x74
main.serverInterceptor({0x104e4ea60?, 0x14000102750}, {0x104dd2d40, 0x14000046340}, 0x1400011faa8?, 0x104a98c24?)
        /Users/michaelaltshuler/GolandProjects/hockney/products/hock/ap/resources/users/v1/server.go:62 +0x3c
github.com/jaebrownn/hockney/protobuf/go/hock/ap/resources/users/v1._UsersService_GetUser_Handler({0x104dd7cc0?, 0x1052cdd08}, {0x104e4ea60, 0x14000102750}, 0x14000140000, 0x104e44430)
        /Users/michaelaltshuler/GolandProjects/hockney/protobuf/go/hock/ap/resources/users/v1/users_grpc.pb.go:214 +0x138
google.golang.org/grpc.(*Server).processUnaryRPC(0x140000d63c0, {0x104e52278, 0x14000003860}, 0x140005a4000, 0x140000a08a0, 0x10528e3f8, 0x0)
        /Users/michaelaltshuler/go/pkg/mod/google.golang.org/[email protected]/server.go:1295 +0x9c4
google.golang.org/grpc.(*Server).handleStream(0x140000d63c0, {0x104e52278, 0x14000003860}, 0x140005a4000, 0x0)
        /Users/michaelaltshuler/go/pkg/mod/google.golang.org/[email protected]/server.go:1636 +0x82c
google.golang.org/grpc.(*Server).serveStreams.func1.2()
        /Users/michaelaltshuler/go/pkg/mod/google.golang.org/[email protected]/server.go:932 +0x84
created by google.golang.org/grpc.(*Server).serveStreams.func1
        /Users/michaelaltshuler/go/pkg/mod/google.golang.org/[email protected]/server.go:930 +0x290

And when navigating to my getUser method from this stack trace it takes me to the line err = dsnap.DataTo(user) in the code below:

dsnap, err := clients.Firestore.Doc(req.GetName()).Get(ctx)
    //user message to return
    user := &pb.User{}
    err = dsnap.DataTo(user) // This line 

if err != nil {
        return nil, err
    }

I know this is a very long winded way of asking for help with this question. I have found very little resources online to deal with this issue, and I'm hoping this makes some sense and someone could guide me in the right direction. Thank you!


Solution

  • I seemed to have fixed the problem and it was a lot more simple than I originally thought. When writing the test method I was specifying the user as User: "UserID", however, the server expected a path to the firestore document ref, and needed the input User: "user/UserID".