This document guides on how to upgrade between significant versions of Gruf.
In 2.15.x, autoloading of Gruf controllers via Zeitwerk was added. This, however, means that if you had Gruf controllers in your controllers path (default of app/rpc) that did not follow the standard autoloading structure, you will get loading errors on instantiation of gruf. A common example of this is naming the class a different name than the file (when underscored). It is recommended to follow Zeitwerk's naming structure when creating controller files.
For example, the following Gruf controller named ::MyService::Rpc::ProductsController
should be in the path:
app/rpc/my_service/rpc/products_controller.rb
.
Upgrading to Gruf 2.15 when you are not in compliance with the standard Zeitwerk/Rails file and class naming structures will require getting your service into compliance to do so properly.
Gruf 2.12.x fixes a bug in prior versions where interceptors were executed in FILO order, rather than FIFO order, which was the documented behavior. If you are using interceptors, and you believed the order of their execution was in FILO order purposefully, you will need to adjust the order of adding interceptors to continue your desired behavior after upgrading.
Gruf 2.5 introduces the concept of Client Error subclasses, such as:
Gruf::Client::Errors::InvalidArgument
Gruf::Client::Errors::NotFound
- etc
These closely map with their GRPC::BadStatus
counterparts, and each subclass Gruf::Client::Error
. This should be
fully backwards compatible with your existing client error handling code, as the original exception through is still
available via .error
on the raised exception.
However, one change is that Gruf will now catch StandardError
and GRPC::Core::CallError
exceptions at the client
boundary, and translate them into Gruf::Client::Errors::Internal
exceptions in the client. If you have code that
does not expect this case, you will need to adjust accordingly.
Gruf 2.0 is a major shift from Gruf 1.0. The following summarizes the changes:
Controllers now bind to services:
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
Note the following changes:
- Methods no longer have request/call arguments. These are now accessible via the new
Gruf::Controllers::Request
object (more on that later). - Controllers "bind" to a Service. This allows thread safety for controllers, and provides a clear separation layer between Gruf functionality and the generated service stubs by gRPC.
- You now extend your services with
Gruf::Controllers::Base
- Furthermore, the
fail!
method no longer requiresreq
andcall
to be passed in
New in methods is the request
instance variable, which provides thread-safe access to the request
and its context. The request object has the following methods:
request.message
- Similar to the firstreq
argument in gRPC service stubs.request.messages
- If using a client streamer call, this will allow you to pass a block in that will execute for each streamed client message. (It's similar tocall.each_remote_read
in gRPC). For non-client streaming messages, this will return an array of messages past. Requests with only one message will have an array of one item.request.active_call
- The currently exposed view for theGRPC::ActiveCall
objectrequest.method_key
- The Symbol name of the currently executing methodrequest.service_key
- A stats-friendly name of the namespaced service being executed againstrequest.method_name
- A stats-friendly name of the service and method being executed against
Before, after, around, and outer around hooks have been removed in favor of the new
Gruf::Interceptors::ServerInterceptor
. This paves the way for Gruf to support Client interceptors
and also support the native gRPC interceptors added in gRPC 1.7.
Furthermore, Gruf 1.x had three different types of hooks with slightly different signatures: Authentication, Instrumentation, and generic hooks. Those have all been consolidated into interceptors.
Interceptors behave very similarly to outer around hooks, with the following changes:
- They are now provided an instance of
Gruf::Controllers::Request
, which contains request information such as the request message(s), active call, metadata, service name, method key, and more. - Their
call
method has no arguments, and must yield control that will return the result of the method call. - They execute in a FIFO execution order. Combined with the collapsing of the different hook types, this allows you to completely control the execution order of all interceptors in your service. For example, you can now have auth before or after instrumentation, move metadata injection earlier, etc.
An base interceptor looks like this:
class MyInterceptor < ::Gruf::Interceptors::ServerInterceptor
def call
yield
end
end
From there, the interceptor can be added to the server manually (if not executing via bundle exec gruf
):
server = Gruf::Server.new
server.add_interceptor(MyInterceptor, option_foo: 'value 123')
Or, alternatively, by passing them into the interceptors
configuration hash:
Gruf.configure do |c|
c.interceptors.use(MyInterceptor, option_foo: 'value 123')
end
Important for instrumentation-related tasks, interceptors can now use a new Gruf::Interceptors::Timer
class, that exposes a time
method that takes a block and will return a
Gruf::Interceptors::Timer::Result
object.
This object will have the following attributes:
message
- The result of the block called; either the protobuf message returned, or the GRPC errorelapsed
- The time elapsed for the block call, in mssuccessful?
- Whether or not the request was successful
This allows interceptors to fine-tune their time measurements, accounting properly for timing variance dependent on where it lies in the interceptor chain.
- The request logging interceptor now defaults to the
logstash
formatter Gruf::Server
no longer supports specifying services in the constructor; they are done through theadd_service
methodGruf.servers_path
has been removed in favor ofGruf.controllers_path
Instrumentation hooks no longer have instance variables set on them during each call - this
is to move to dependency injection via a new RequestContext
object that is now passed in
as the first argument to the call
method on the instrumentation strategy.
You'll need to adjust your strategies to accommodate for this, by using the new RequestContext
argument instead of relying on the accessors on the strategy class itself.
If you're doing any custom request logging in hooks, you'll want to disable that in favor
of the new Gruf::Instrumentation::RequestLogging::Hook
that does that for you.
If you're desiring JSON or Logstash-formatted logs, make sure to set the following for config:
Gruf.configure do |c|
c.instrumentation_options[:request_logging] = {
formatter: :logstash,
log_parameters: false
}
end
If you want to log parameters, we recommend setting a blocklist to ensure you don't accidentally log sensitive data.
We also recommend blocklisting parameters that may contain very large values (such as binary or json data).