Gruf gRPC Ruby Framework

repository·main·Indexed 20 days ago

https://github.com/bigcommerce/gruf

A high-level Ruby framework that wraps the official gRPC Ruby library to provide production-ready abstractions for building gRPC servers and clients. Gruf offers abstracted controllers with request context, built-in server authentication, interceptor support, and robust error handling. While compatible with Ruby on Rails, it is framework-agnostic and can be used with Grape or dry-rb. It includes a CLI for server management and a Gruf::Client for streamlined RPC calls with automatic request mapping and instrumented responses.

Tokens
8.9K
Snippets
26
Records
40
Agent score
71%

What's inside Gruf

  1. Overview of the Gruf gRPC Ruby Framework

    main

    Gruf is a Ruby framework designed to wrap the standard gRPC Ruby library, providing a more streamlined integration for Ruby and Ruby on Rails applications. It abstracts the complexities of gRPC to help developers build services efficiently at scale.

    Key features include:

    • Abstracted Controllers: Provides request context support similar to web frameworks.
    • Interceptors: Full support for interceptors with timing and unified request context.
    • Error Handling: Robust client error handling and error data serialization in output metadata (preserving gRPC BadStatus codes).
    • Authentication: Built-in server authentication via interceptors, including support for basic auth with multiple keys and TLS.
    • Observability: Includes server and client execution timings in responses.
    • Flexibility: While it works well with Rails, it is not Rails-specific and can be used with other frameworks like Grape or dry-rb.
  2. Upgrade to Gruf 2.0.0: Services are now Controllers

    main

    In Gruf 2.0.0, the architecture shifted so that controllers bind to services. This provides thread safety and separates Gruf functionality from gRPC service stubs.

    Key Changes:

    • Extend your services with Gruf::Controllers::Base.
    • Use the bind method to link the controller to the service.
    • Methods no longer accept req or call arguments; instead, they use the request instance variable.
    • The fail! method no longer requires req and call arguments.

    Example:

    class ThingController < ::Gruf::Controllers::Base
      bind ::Rpc::ThingService::Service
    
      def get_thing
        thing =  Rpc::Thing.new(id: request.message.id, name: 'Foo')
        Rpc::GetThingResponse.new(thing: thing)
      end
    end
    class ThingController < ::Gruf::Controllers::Base
      bind ::Rpc::ThingService::Service
    
      ##
      # Get the thing
      #
      def get_thing
        thing =  Rpc::Thing.new(id: request.message.id, name: 'Foo')
        Rpc::GetThingResponse.new(thing: thing)
      end
    end
  3. Getting started with Gruf

    main

    To begin using Gruf, refer to the official gruf wiki for detailed setup instructions and implementation guides.

    For a practical implementation example, you can explore the gruf-demo repository, which is a demonstration Rails application showing how to integrate Gruf into an existing Rails environment.

  4. Upgrade to Gruf 2.15.x: Fix Zeitwerk Autoloading Errors

    main

    In version 2.15.x, Gruf introduced autoloading for controllers via Zeitwerk. If your controller files do not follow standard Zeitwerk/Rails naming conventions (where the file path matches the constant path), you will encounter loading errors.

    Requirement: Ensure your controller class names match their underscored file paths.

    Example: A controller named ::MyService::Rpc::ProductsController must be located at app/rpc/my_service/rpc/products_controller.rb.

  5. Upgrade to Gruf 2.5.0: New Client Error Subclasses and Exception Handling

    main

    Gruf 2.5.0 introduced Gruf::Client::Error subclasses that map to GRPC::BadStatus counterparts.

    New Error Classes:

    • Gruf::Client::Errors::InvalidArgument
    • Gruf::Client::Errors::NotFound
    • (and others)

    Breaking Change in Exception Handling: Gruf now catches StandardError and GRPC::Core::CallError at the client boundary and translates them into Gruf::Client::Errors::Internal. If your client-side error handling does not account for Gruf::Client::Errors::Internal for these cases, you must update your code.

    Compatibility: Existing error handling remains functional as the original exception is still accessible via the .error method on the raised exception.

  6. Use Gruf::Outbound::RequestContext in client interceptors

    main

    When implementing a ClientInterceptor, the call method provides a request_context object. This object abstracts the differences between various gRPC communication patterns (unary, client streaming, server streaming, and bidirectional streaming) into a consistent interface.

    Depending on the call type, the request_context contains:

    • type: The communication pattern (:request_response, :client_streamer, :server_streamer, or :bidi_streamer).
    • requests: An array or enumerable of the requests being sent.
    • call: The GRPC::ActiveCall object.
    • method: The gRPC method being invoked.
    • metadata: A hash of the outgoing metadata.
  7. Use the request context to share data between interceptors and controllers

    main

    The context attribute on a Gruf::Controllers::Request is an ActiveSupport::HashWithIndifferentAccess. This is the intended mechanism for passing arbitrary key/value data through the request lifecycle.

    Pattern:

    1. An Interceptor performs an action (like authentication) and writes data to request.context[:user_id] = 123.
    2. The Controller reads that data using request.context[:user_id] to perform business logic.
  8. Manage framework hooks with Gruf::Hooks::Registry

    main

    The Gruf::Hooks::Registry class is responsible for managing the lifecycle and execution order of hooks within the Gruf framework. You can use it to register new hooks, remove existing ones, or precisely control their execution order by inserting them before or after specific classes.

    When the framework is ready to process a request, it calls prepare on the registry to instantiate the registered hook classes with their provided options.

    registry = Gruf::Hooks::Registry.new
    registry.use(MyHookClass, some_option: true)
    # The registry can then be used to prepare hook instances
    hooks = registry.prepare
  9. How Gruf::Client abstracts gRPC calls

    main

    Gruf::Client acts as a wrapper (using SimpleDelegator) around a standard gRPC stub. It streamlines the developer experience by:

    1. Automatic Request Mapping: It looks up the RPC descriptor for a given method name and automatically instantiates the correct protobuf request class using the provided params hash.
    2. Instrumented Responses: Instead of returning raw gRPC objects, it returns a Gruf::Response which includes execution timing and a clean interface to the response message.
    3. Error Handling: It uses an ErrorFactory to intercept gRPC errors and attempt to deserialize rich error messages sent via trailing metadata (defaulting to JSON).
  10. Handle errors and interceptors in Gruf controllers

    main

    The Gruf::Controllers::Base#call method manages the execution flow and error handling for gRPC requests:

    1. Interceptors: It wraps the method execution in Interceptors::Context.new(@interceptors).intercept!, allowing global or request-specific interceptors to run.
    2. GRPC::BadStatus: If a GRPC::BadStatus is raised, it is passed through (re-raised) to allow interceptors (like timers) to catch it.
    3. Standard Errors: For StandardError or GRPC::Core::CallError, the controller:
      • Optionally sets debug info if Gruf.backtrace_on_error is enabled.
      • Determines the error message based on the Gruf.use_exception_message setting.
      • Calls fail!(:internal, :unknown, error_message) to return a structured gRPC error.

    When implementing methods, you can rely on these built-in behaviors to ensure consistent error responses across your service.

  11. Initialize Gruf via Zeitwerk autoloading

    main

    Gruf uses the Zeitwerk gem to lazily autoload all files within the lib directory. This ensures that constants are only loaded when they are first accessed, improving startup performance. When using Gruf, you can interact with the Gruf module directly, and its components will be loaded automatically. Note that certain files, such as the Rails railtie and the health controller, are explicitly ignored by the autoloader to prevent conflicts or to allow for specific integration patterns.

    require 'gruf'
    
    # Constants and modules within Gruf will be autoloaded on use
    # e.g., Gruf::SomeController
  12. Configure SSL/TLS for Gruf::Server

    main

    To enable SSL/TLS, set use_ssl: true in the server options and provide the paths to your key and certificate files using ssl_key_file and ssl_crt_file. If use_ssl is false, the server will run in insecure mode.

    server = Gruf::Server.new(
      use_ssl: true,
      ssl_key_file: '/path/to/server.key',
      ssl_crt_file: '/path/to/server.crt'
    )