gritz-rails 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cdb05adc6d6f492115eb0e7d9de033bd3e4509a65f60db732377e254b7f28d5f
4
+ data.tar.gz: 927579595d4645896cac9dfe6f8514ccd2e52abf94ad10f1cb77c07158dff833
5
+ SHA512:
6
+ metadata.gz: dac749d3fc8f50ac2716e3de2557f5fd62b9cb61cd62c5fdfaa0a37dbaa76d690279a4de3cf8cd0baf53128878e08cbe7228e940317f7ab2b1c8a896f08ec6f1
7
+ data.tar.gz: 02d442a42de49ba6a6c0a27587e74b380f510c6baebb28b9ab6c79318774cb75cd1f878f6af4c08d856d8923cd0b9081b5b2f7e31ae7ef34e721bbb8d832a21f
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 ydah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Gritz Rails
2
+
3
+ Rails integration for [Gritz](https://github.com/gritzrpc/gritz): RPC controller loading, generators, complete-RPC execution and single-process development reloading. Requires CRuby 3.3+, Rails 8.0 or 8.1, and Gritz 0.5.0. Runtime dependencies are `gritz-core` and `railties`; applications choose their transport separately.
4
+
5
+ ## Install
6
+
7
+ ```ruby
8
+ gem "gritz", "~> 0.5.0"
9
+ gem "gritz-rails", "~> 0.1.0"
10
+ ```
11
+
12
+ The first `gritz-rails` release is prepared for the project owner. Until it is published, use `gem "gritz-rails", git: "https://github.com/gritzrpc/gritz-rails.git", branch: "main"`.
13
+
14
+ ```sh
15
+ bin/rails generate gritz:install
16
+ # Generate protobufs into lib/protos, then generate a bound controller:
17
+ bin/rails generate gritz:controller Greeter SayHello --service Helloworld::Greeter::Service
18
+ bin/gritz routes
19
+ bin/gritz check
20
+ bin/gritz start
21
+ ```
22
+
23
+ Implement each generated action using `request.message` or `request.each_message`, and `stream.write` for response streams. The controller generator accepts RPC names and `--service` names; it adds the controller to `config/gritz.rb`. Set `strict_routes true` after all actions are implemented.
24
+
25
+ ## Rails lifecycle
26
+
27
+ Generated `config/gritz.rb` loads the application through `rails_app`, selects four workers in production and zero elsewhere, and registers controllers. Additional settings come after `rails_app`, including explicit `reflection false` or `true`. Rails development enables Reflection by default; other environments keep the core's disabled default.
28
+
29
+ Every complete application RPC runs inside the Rails executor. Development uses the Rails reloader and resolves controller constants after reload. Active Record connections return to their pools, query caches finish and CurrentAttributes reset on successful and failed calls. Application threads created inside a handler must use Rails' own executor wrapping. See the [Rails execution guide](https://guides.rubyonrails.org/threading_and_code_execution.html).
30
+
31
+ Application code is eager loaded before fork. In prefork mode, all Active Record pools disconnect after eager loading, before Ruby warmup, and again before each fork. Ruby's Rails fork tracking remains active. Development reloading requires `workers 0`; changing protobuf definitions, bound services or registered routes requires a server restart. Production code changes use Gritz's `USR2` fresh-interpreter replacement.
32
+
33
+ `app/rpc` participates in Rails autoloading and eager loading. Generated protobuf files in `lib/protos` are excluded from both Zeitwerk loaders and required explicitly by the generated initializer. Keep generated files outside normal autoload directories.
34
+
35
+ For an already initialized application, use `Gritz::Rails.install(config, application: Rails.application)` once before starting the server. Match Active Record pool capacity to RPC concurrency with `RAILS_MAX_THREADS` or your database configuration.
36
+
37
+ ## Examples and migration
38
+
39
+ The [Rails catalog sample](examples/rails_app) serves SQLite-backed unary and streaming RPCs with four production workers. It includes the [memory validation](docs/reports/T5-07-rails-memory.md).
40
+
41
+ Existing Gruf applications can use the optional `Gritz::Compat::Gruf` controller and interceptor adapter in `gritz-core`. See the [migration guide](https://github.com/gritzrpc/gritz-core/blob/main/docs/guides/migrating-from-gruf.md).
42
+
43
+ ## Development
44
+
45
+ ```sh
46
+ bundle install
47
+ COVERAGE=1 bundle exec rake
48
+ bundle exec rubocop
49
+ bundle exec bundler-audit check --update
50
+ bundle exec rake build
51
+ ```
52
+
53
+ CI tests Ruby 3.3, 3.4 and 4.0 against Rails 8.0 and 8.1. Tests include real RPCs, file-change reloading, all-pool fork cleanup, executable generated configuration and Linux four-worker sample lifecycle. Sibling checkout overrides follow [CONTRIBUTING.md](CONTRIBUTING.md).
54
+
55
+ The initial package is `pkg/gritz-rails.gem`. Stop before its first tag/publication and configure [Trusted Publishing](docs/guides/releasing.md) after the owner releases it.
56
+
57
+ ## License
58
+
59
+ [MIT](LICENSE.txt).
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Gritz
6
+ module Generators
7
+ # Generates a controller bound to an application's generated service.
8
+ # @api public
9
+ class ControllerGenerator < ::Rails::Generators::NamedBase
10
+ source_root File.expand_path("templates", __dir__)
11
+ desc "Generate an RPC controller and register it in config/gritz.rb."
12
+ argument :actions, type: :array, default: [], banner: "RPC_ACTIONS"
13
+ class_option :service, type: :string, desc: "Generated service class (defaults to NAME::Service)"
14
+
15
+ def create_controller
16
+ raise Thor::Error, "service must be a Ruby constant name" unless /\A[A-Z]\w*(?:::[A-Z]\w*)*\z/.match?(service_class_name)
17
+ unless actions.all? { |action| /\A[a-z_]\w*\z/.match?(action.underscore) }
18
+ raise Thor::Error, "action must be a Ruby method name"
19
+ end
20
+
21
+ template "controller.rb.tt", "app/rpc/#{file_path}_controller.rb"
22
+ append_to_file "config/gritz.rb", "\nregister_controller #{class_name}Controller\n"
23
+ end
24
+
25
+ private
26
+
27
+ def service_class_name
28
+ (options[:service] || "#{class_name}::Service").delete_prefix("::")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>Controller < Gritz::Controller
4
+ bind ::<%= service_class_name %>
5
+ <% actions.each do |action| -%>
6
+
7
+ def <%= action.underscore %>
8
+ fail!(:unimplemented, "Implement <%= action.underscore %>")
9
+ end
10
+ <% end -%>
11
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Gritz
6
+ module Generators
7
+ # Creates the configuration and binstub for a Rails RPC server.
8
+ # @api public
9
+ class InstallGenerator < ::Rails::Generators::Base
10
+ source_root File.expand_path("templates", __dir__)
11
+ desc "Install Gritz configuration, protobuf loading, and bin/gritz."
12
+
13
+ def install
14
+ template "gritz.rb", "config/gritz.rb"
15
+ copy_file "initializer.rb", "config/initializers/gritz.rb"
16
+ copy_file "gritz", "bin/gritz"
17
+ chmod "bin/gritz", 0o755
18
+ empty_directory "lib/protos"
19
+ create_file "lib/protos/.keep"
20
+ empty_directory "app/rpc"
21
+ create_file "app/rpc/.keep"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "gritz/rails"
6
+
7
+ exit Gritz::CLI.new(launch: true).run(ARGV)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gritz/rails"
4
+
5
+ rails_app
6
+ workers ::Rails.env.production? ? 4 : 0 # rubocop:disable Style/RedundantConstantBase -- The config is evaluated inside Gritz::DSL.
7
+ threads 16
8
+
9
+ # Add controllers with: bin/rails generate gritz:controller Greeter SayHello --service Example::Greeter::Service
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # protoc output does not follow Zeitwerk's naming rules.
4
+ $LOAD_PATH.unshift Rails.root.join("lib/protos").to_s
5
+ Dir[Rails.root.join("lib/protos/**/*_pb.rb")].each { |path| require path }
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gritz
4
+ module Rails
5
+ # Keeps Rails resources and reload interlocks scoped to the entire RPC.
6
+ # @api public
7
+ class Executor
8
+ def initialize(app, application:, development: false)
9
+ @app = app
10
+ @executor = development ? application.reloader : application.executor
11
+ end
12
+
13
+ def call(context)
14
+ @executor.wrap { @app.call(context) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gritz
4
+ module Rails
5
+ # Adds RPC controllers to Rails loading while keeping protoc output explicit.
6
+ # @api private
7
+ class Railtie < ::Rails::Railtie
8
+ initializer "gritz.paths", before: :set_autoload_paths do |application|
9
+ application.config.paths.add("app/rpc", eager_load: true)
10
+ application.autoloaders.each { |loader| loader.ignore(application.root.join("lib/protos")) }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gritz
4
+ module Rails
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gritz/core"
4
+ require "rails"
5
+ require "active_support/fork_tracker"
6
+ require_relative "rails/version"
7
+ require_relative "rails/executor"
8
+ require_relative "rails/railtie"
9
+
10
+ module Gritz
11
+ module Rails
12
+ # Installs Rails execution and fork lifecycle hooks into one server configuration.
13
+ # @api public
14
+ def self.install(config, application: ::Rails.application)
15
+ raise ConfigurationError, "rails_app requires an initialized Rails application" unless application&.initialized?
16
+
17
+ installed = config.middleware.entries.find { |entry| entry.middleware == Executor }
18
+ if installed
19
+ raise ConfigurationError, "Rails integration is already installed for another application" unless installed.options[:application].equal?(application)
20
+
21
+ return config
22
+ end
23
+
24
+ development = ::Rails.env.development?
25
+ config.reflection = true if development
26
+ first = config.middleware.entries.first
27
+ if first
28
+ config.middleware.insert_before(first.middleware, Executor, application:, development:)
29
+ else
30
+ config.middleware.use(Executor, application:, development:)
31
+ end
32
+ disconnect_pools = lambda do |_index = nil|
33
+ if defined?(ActiveRecord::Base)
34
+ ActiveRecord::Base.connection_handler.connection_pool_list(:all).each(&:disconnect!)
35
+ end
36
+ end
37
+ config.add_preloader do
38
+ raise ConfigurationError, "Rails development mode requires workers 0" if development && config.workers.positive?
39
+
40
+ application.eager_load!
41
+ config.controllers.map! { |controller| reloadable_controller(controller) } if development
42
+ disconnect_pools.call if config.workers.positive?
43
+ end
44
+ config.add_hook(:before_fork, &disconnect_pools)
45
+ config
46
+ end
47
+
48
+ # Route descriptors stay fixed, while the controller constant is resolved after Rails reloads.
49
+ # @api private
50
+ def self.reloadable_controller(controller)
51
+ name = controller.name
52
+ raise ConfigurationError, "Rails development mode requires a named controller" unless name
53
+
54
+ Class.new(Gritz::Controller) do
55
+ bind controller.service_class
56
+ define_singleton_method(:name) { name }
57
+ define_singleton_method(:to_s) { name }
58
+ define_singleton_method(:action_defined?) { |action| name.constantize.action_defined?(action) }
59
+ define_singleton_method(:new) { |**options| name.constantize.new(**options) }
60
+ end
61
+ end
62
+
63
+ # @api public
64
+ module ConfigurationDSL
65
+ def rails_app(path = "config/environment.rb")
66
+ require File.expand_path(path)
67
+ Gritz::Rails.install(@config)
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ Gritz::DSL.include(Gritz::Rails::ConfigurationDSL)
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gritz-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: gritz-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 0.5.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 0.5.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: railties
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '9'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '8.0'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '9'
46
+ description: Rails autoloading, RPC execution, generators and safe preloading for
47
+ Gritz.
48
+ email:
49
+ - t.yudai92@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - CHANGELOG.md
55
+ - LICENSE.txt
56
+ - README.md
57
+ - lib/generators/gritz/controller/controller_generator.rb
58
+ - lib/generators/gritz/controller/templates/controller.rb.tt
59
+ - lib/generators/gritz/install/install_generator.rb
60
+ - lib/generators/gritz/install/templates/gritz
61
+ - lib/generators/gritz/install/templates/gritz.rb
62
+ - lib/generators/gritz/install/templates/initializer.rb
63
+ - lib/gritz/rails.rb
64
+ - lib/gritz/rails/executor.rb
65
+ - lib/gritz/rails/railtie.rb
66
+ - lib/gritz/rails/version.rb
67
+ homepage: https://github.com/gritzrpc/gritz-rails
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ allowed_push_host: https://rubygems.org
72
+ homepage_uri: https://github.com/gritzrpc/gritz-rails/
73
+ source_code_uri: https://github.com/gritzrpc/gritz-rails
74
+ changelog_uri: https://github.com/gritzrpc/gritz-rails/blob/main/CHANGELOG.md
75
+ rubygems_mfa_required: 'true'
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '3.3'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 4.0.16
91
+ specification_version: 4
92
+ summary: Rails integration for Gritz RPC servers
93
+ test_files: []