thrifter 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fd99b7900e55cc7abcfe145198cb184b4dd27d28
4
+ data.tar.gz: b5aeb9adef656c411b43327a619496f0dd46b429
5
+ SHA512:
6
+ metadata.gz: 30d3d385ef740047a5dae0b0f704518a5e9c77e551bd29d7cf991a65b33fb5255a4b246571e6088ae36a457bb5ca37a9d90dda2c949efd50f3bfcb7431ce09be
7
+ data.tar.gz: c0c644e11697991a4f2187dd505906a53de4829d74f7653b7f65ef25965ddf6649a78caca0f5d5e05a64ab9385d805f6c981a6cadbc380c43b898391f73711e0
@@ -0,0 +1,3 @@
1
+ .vagrant
2
+ .git
3
+ tmp
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
@@ -0,0 +1,15 @@
1
+ FROM ruby:2.1
2
+
3
+ RUN mkdir -p /app/lib/thrifter
4
+
5
+ WORKDIR /app
6
+
7
+ # Add only the files that are required to run bundle installer.
8
+ # Remaining library code will be provided via a volume
9
+ ADD Gemfile /app/
10
+ ADD thrifter.gemspec /app/
11
+ ADD lib/thrifter/version.rb /app/lib/thrifter/
12
+
13
+ RUN bundle install -j $(nproc)
14
+
15
+ CMD [ "bundle", "console" ]
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in thrifter.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 ahawkins
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ THRIFT:=vendor/gen-rb/test_types.rb
2
+
3
+ BUNDLE:=tmp/bundle
4
+ DOCKER_IMAGES:=tmp/docker_images
5
+
6
+ BUNDLE_IMAGE:=thrifter/bundle
7
+
8
+ # CircleCI does not support --rm, so if the environment variable has
9
+ # value, then don't include --rm.
10
+ ifneq ($(shell echo $$CIRCLECI),)
11
+ DOCKER_RUN:=docker run -it
12
+ else
13
+ DOCKER_RUN:=docker run --rm -it
14
+ endif
15
+
16
+ $(THRIFT): test.thrift
17
+ mkdir -p $(@D)
18
+ thrift -o vendor --gen rb test.thrift
19
+
20
+ $(DOCKER_IMAGES):
21
+ mkdir -p $(@D)
22
+ touch $@
23
+
24
+ $(BUNDLE): Dockerfile thrifter.gemspec $(DOCKER_IMAGES)
25
+ docker build -t $(BUNDLE_IMAGE) .
26
+ docker inspect -f '{{ .Id }}' $(BUNDLE_IMAGE) >> $(DOCKER_IMAGES)
27
+
28
+ .PHONY: test
29
+ # For quick and easy access to the most common thing
30
+ test: test-lib
31
+
32
+ .PHONY: test-lib
33
+ test-lib: $(THRIFT) $(BUNDLE)
34
+ $(DOCKER_RUN) -v $(CURDIR):/app $(BUNDLE_IMAGE) bundle exec rake test
35
+
36
+ .PHONY: test-ci
37
+ test-ci: test-lib test-monkey
38
+
39
+ .PHONY: test-monkey
40
+ test-monkey: teardown
41
+ docker run -d -v $(CURDIR):/app --name server $(BUNDLE_IMAGE) script/server
42
+ $(DOCKER_RUN) --link server:server -v $(CURDIR):/app \
43
+ $(BUNDLE_IMAGE) script/monkey-client server:9090
44
+
45
+ .PHONY:
46
+ release:
47
+ bundle exec rake release
48
+
49
+ .PHONY: treadown
50
+ teardown:
51
+ -docker stop server 2> /dev/null
52
+ -docker rm server 2> /dev/null
53
+
54
+ .PHONY: clean
55
+ clean: $(DOCKER_IMAGES) teardown
56
+ -sort -u $(DOCKER_IMAGES) | xargs --no-run-if-empty docker rmi 2> /dev/null
57
+ rm -f $(DOCKER_IMAGES) $(APP)
@@ -0,0 +1,283 @@
1
+ # Thrifter
2
+
3
+ Thrifter addresses the shortcoming in the official library for
4
+ production uses. It's most important features are:
5
+
6
+ * Thread safe via connection pool
7
+ * Safe for use in long running processes
8
+ * Simple RPC queuing
9
+ * Retry support
10
+ * Proper timeouts by default
11
+ * Metrics
12
+ * Better error handling
13
+ * Middleware
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'thrifter'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install thrifter
30
+
31
+ ## Usage
32
+
33
+ `Thrifer` is a factory (similar to `DelegateClass`) for building
34
+ client classes. It can be used like `DelegateClass` or like `Struct`.
35
+ The result is subclasses of `Thrifter::Client`. The classes use the
36
+ same methods to make RPCs. For example if the thrift service has a
37
+ `fooBar` RPC, the generated class has a `fooBar` method to invoke the
38
+ RPC. Here are some examples.
39
+
40
+ ```ruby
41
+ # Struct style
42
+ ServiceClient = Thrifter.build(MyService::Client)
43
+
44
+ The struct style take a block as well
45
+ ServiceClient = Thrifer.build(MyService::Client) do
46
+ def custom_method(args)
47
+ # something something
48
+ end
49
+ end
50
+
51
+ # Delegate class style (easiest to add abstractions)
52
+ class MyServiceClient < Thrifter.build(MyService::Client)
53
+ def custom_method(args)
54
+ # something something
55
+ end
56
+ end
57
+ ```
58
+
59
+ ### Configuration
60
+
61
+ `Thrifter` uses a configuration object on each class to track
62
+ dependent objects. Configured objects are injected into each instance.
63
+ This makes it easy to configure classes in different environment (e.g.
64
+ production vs test). All settings are documented below. `uri` is the
65
+ most important! It must be set to instantiate clients.
66
+
67
+ ```ruby
68
+ class MyClient < Thrifer.build(MyService::Client)
69
+ # Thrift specific things
70
+ config.transport = Thrift::FramedTransport
71
+ config.protocol = Thrift::BinaryTransport
72
+
73
+ # Pool settings
74
+ config.pool_size = 12
75
+ config.pool_timeout = 0.15
76
+
77
+ # Network Settings
78
+ config.rpc_timeout = 0.15
79
+
80
+ # Required to instantiate the client!
81
+ config.uri = 'tcp://foo:2383'
82
+ end
83
+
84
+ #The common block form is supported as well
85
+ MyClient.configure do |config|
86
+ # ... same as above
87
+ end
88
+ ```
89
+
90
+ ### RPC Metrics with Statsd
91
+
92
+ Statsd metrics are **opt-in**. By default, `Thrifter` sets the statsd
93
+ client to a null implementation. If you want metrics, set
94
+ `config.statsd` to an object that implements the [statsd-ruby][]
95
+ interface. `Thrifter` emits the following metrics:
96
+
97
+ * time on each rpc calls
98
+ * number of `Thrift::TransportException`
99
+ * number of `Thrift::ProtocolExeption`
100
+ * number of `Thrift::ApplicationExeption`
101
+ * number of `Timeout::Error`
102
+ * number of generic errors (e.g. none of the above known errors)
103
+
104
+ It's recommended that the `statsd` object do namespacing
105
+ (statsd-ruby has it built in). This ensures client metrics don't
106
+ get intermingled with wider application metrics. Here's an example:
107
+
108
+ ```ruby
109
+ ServiceClient = Thrifter.build(MyService::Client)
110
+ # Now in production.rb
111
+ ServiceClient.config.statsd = Statsd.new namespace: 'my_service'
112
+ ```
113
+
114
+ ### RPC Queuing
115
+
116
+ Certain systems may need to queue RPCs to other systems. This is only
117
+ useful for `void` RPCs or for when an outside system may be flaky.
118
+ Assume `MyService` has a `logStats` RPC. Your application is producing
119
+ stats that should make it upstream, but there are intermitent network
120
+ problems effeciting stats collection. Include `Thrift::Queueing` and
121
+ any RPC will automatically be sent to sidekiq for eventual processing.
122
+
123
+ ```ruby
124
+ class ServiceClient < Thrifter.build(MyService::Client)
125
+ include Thrifter::Queuing
126
+ end
127
+ ```
128
+
129
+ Now instances of `ServiceClient` now respond to `queued`. This returns
130
+ a queue based instance. All RPC methods will work as usual. Here's an
131
+ example:
132
+
133
+ ```ruby
134
+ # Assume client is an instance of ServiceClient
135
+ my_service.queued.logStats({ 'users' => 5 })
136
+
137
+ # Naturally the block form may be used as well
138
+ my_service.queued do |queue|
139
+ queue.logStats({ 'sessions' => 50 })
140
+ queue.logStats({ 'posts' => 30 })
141
+ end
142
+ ```
143
+
144
+ All RPCs will be sent to the `thrift` sidekiq queue. They will follow
145
+ default sidekiq retry backoff and the like.
146
+
147
+ ### RPC Retrying
148
+
149
+ Systems have syncrhonous RPCs. Unfortunately sometimes these don't
150
+ work for whatever reason. It's good practice to retry these RPCs
151
+ (within certain limits) if they don't succeed the first time.
152
+ `Thrift::Retriable` is perfect for this use case.
153
+
154
+ ```ruby
155
+ class ServiceClient < Thrifter.build(MyService::Client)
156
+ include Thrifter::Retriable
157
+ end
158
+ ```
159
+
160
+ ```ruby
161
+ # Assume client is an instance of ServiceClient
162
+
163
+ # logStats will be retried 3 times at 0.1 second intervals if any
164
+ # known thrift or network errors happen.
165
+ my_service.with_retry.logStats({ 'users' => 5 })
166
+
167
+ # These settings can be customized by the retriable method.
168
+ my_sevice.with_retry({tries: 10, delay: 0.3 }).logStats({ 'sessions' => 50 })
169
+
170
+ # Naturally the block form may be used as well
171
+ my_service.with_retry do |with_retry|
172
+ with_retry.logStats({ 'sessions' => 50 })
173
+ with_retry.logStats({ 'posts' => 30 })
174
+ end
175
+ ```
176
+
177
+ `Thrift::Retriable` is a simple retry solution for syncronous RPCs.
178
+ Look into something like [retriable][] if you want a more robust
179
+ solution for different use cases.
180
+
181
+ ### Middleware
182
+
183
+ The middleware approach is great for providing a flexible extension
184
+ points to hook into the RPC process. `Thrifter::Client` provides a
185
+ middleware implementation to common to many other ruby libraries.
186
+ Middleware can only be customized at the class level. This ensures
187
+ middleware applies when used in extensions.
188
+
189
+ ```ruby
190
+ class MyClient < Thrifter.build(MyService::Client)
191
+ use MyMiddlware
192
+ use MySecondMiddleware
193
+ end
194
+ ```
195
+
196
+ Since middleware must defined at the class level, you should defer
197
+ setting up middleware that depend on objects until process boot. For
198
+ example, if you have `LoggingMiddleware` and you need to log to
199
+ different places depending on environment, you should add the
200
+ middleware in whatever code configurres that environment. Only static
201
+ middleware should be configured directly in the class itself.
202
+
203
+ A middleware must implement the `call` method and accept at least one
204
+ argument to `initialize`. The `call` method recieves a `Thrifer::RPC`.
205
+ `Thrifter::RPC` is a sipmle struct with a `name` and `args` methods.
206
+ Here's an example:
207
+
208
+ ```ruby
209
+ class LoggingMiddleware
210
+ def initialize(app)
211
+ @app = app
212
+ end
213
+
214
+ def call(rpc)
215
+ puts "Running #{rp.name} with #{rpc.args.inspect}"
216
+ @app.call rpc
217
+ end
218
+ end
219
+ ```
220
+
221
+ ### Error Wrapping
222
+
223
+ A lot of things can go wrong in the thrift stack. This means the
224
+ caller may need to deal with a large amount of different exceptions.
225
+ For example, does it really matter if `Thrift::ProtocolException` or
226
+ `Thrift::TransportException` was raied? Can the caller recover from
227
+ either of them? No. So instead of allowing these semantics to
228
+ propogate up abstraction levels, it's better to encapsulate them in a
229
+ single error. This is easily implemented with a middleware and once is
230
+ included in the library. When this middleare is used, all known
231
+ networking & thrift exceptions will be raised as
232
+ `Thrifter::ClientError`.
233
+
234
+ ```ruby
235
+ class MyService < Thrifter.build(MyService::Client)
236
+ use Thrifter::ErrorWrapping
237
+ end
238
+ ```
239
+
240
+ A list of other known error classes can be provided to wrap more than
241
+ the library's known set.
242
+
243
+ ```ruby
244
+ class MyService < Thrifter.build(MyService::Client)
245
+ use Thrifter::ErrorWrapping, [ SomeErrorClass ]
246
+ end
247
+ ```
248
+
249
+ Note, `Thrifter` will still count individual errors as described in
250
+ the metrics section.
251
+
252
+ ### Pinging
253
+
254
+ Components in a system may need to inquire if other systems are
255
+ available before continuing. `Thrifer::Ping` is just that.
256
+ `Thrifter::Ping` assumes the service has a `ping` RPC. If your
257
+ service does not have one (or is named differently) simply implement
258
+ the `ping` method on the class. Any successful response will count as
259
+ up, anything else will not.
260
+
261
+ ```ruby
262
+ class MyService < Thrifer.build(MyService::Client)
263
+ include Thrifer::Ping
264
+
265
+ # Define a ping method if the service does not have one
266
+ def ping
267
+ my_other_rpc
268
+ end
269
+ end
270
+
271
+ # my_service.up? # => true
272
+ ```
273
+
274
+ ## Contributing
275
+
276
+ 1. Fork it ( https://github.com/saltside/thrifter/fork )
277
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
278
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
279
+ 4. Push to the branch (`git push origin my-new-feature`)
280
+ 5. Create a new Pull Request
281
+
282
+ [retriable]: https://github.com/kamui/retriable
283
+ [statsd-ruby]: https://github.com/reinh/statsd
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.test_files = Rake::FileList['test/**/*_test.rb']
6
+ end
7
+
8
+ task default: :test
@@ -0,0 +1,126 @@
1
+ # -*- mode: ruby -*-
2
+ # vi: set ft=ruby :
3
+
4
+ # Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
5
+ VAGRANTFILE_API_VERSION = "2"
6
+
7
+ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
8
+ # All Vagrant configuration is done here. The most common configuration
9
+ # options are documented and commented below. For a complete reference,
10
+ # please see the online documentation at vagrantup.com.
11
+
12
+ # Every Vagrant virtual environment requires a box to build off of.
13
+ config.vm.box = "ubuntu/trusty64"
14
+
15
+ config.vm.provision "docker" do |docker|
16
+ docker.pull_images "ruby:2.1"
17
+ end
18
+
19
+ # Disable automatic box update checking. If you disable this, then
20
+ # boxes will only be checked for updates when the user runs
21
+ # `vagrant box outdated`. This is not recommended.
22
+ # config.vm.box_check_update = false
23
+
24
+ # Create a forwarded port mapping which allows access to a specific port
25
+ # within the machine from a port on the host machine. In the example below,
26
+ # accessing "localhost:8080" will access port 80 on the guest machine.
27
+ # config.vm.network "forwarded_port", guest: 80, host: 8080
28
+
29
+ # Create a private network, which allows host-only access to the machine
30
+ # using a specific IP.
31
+ # config.vm.network "private_network", ip: "192.168.33.10"
32
+
33
+ # Create a public network, which generally matched to bridged network.
34
+ # Bridged networks make the machine appear as another physical device on
35
+ # your network.
36
+ # config.vm.network "public_network"
37
+
38
+ # If true, then any SSH connections made will enable agent forwarding.
39
+ # Default value: false
40
+ # config.ssh.forward_agent = true
41
+
42
+ # Share an additional folder to the guest VM. The first argument is
43
+ # the path on the host to the actual folder. The second argument is
44
+ # the path on the guest to mount the folder. And the optional third
45
+ # argument is a set of non-required options.
46
+ # config.vm.synced_folder "../data", "/vagrant_data"
47
+
48
+ # Provider-specific configuration so you can fine-tune various
49
+ # backing providers for Vagrant. These expose provider-specific options.
50
+ # Example for VirtualBox:
51
+ #
52
+ # config.vm.provider "virtualbox" do |vb|
53
+ # # Don't boot with headless mode
54
+ # vb.gui = true
55
+ #
56
+ # # Use VBoxManage to customize the VM. For example to change memory:
57
+ # vb.customize ["modifyvm", :id, "--memory", "1024"]
58
+ # end
59
+ #
60
+ # View the documentation for the provider you're using for more
61
+ # information on available options.
62
+
63
+ # Enable provisioning with CFEngine. CFEngine Community packages are
64
+ # automatically installed. For example, configure the host as a
65
+ # policy server and optionally a policy file to run:
66
+ #
67
+ # config.vm.provision "cfengine" do |cf|
68
+ # cf.am_policy_hub = true
69
+ # # cf.run_file = "motd.cf"
70
+ # end
71
+ #
72
+ # You can also configure and bootstrap a client to an existing
73
+ # policy server:
74
+ #
75
+ # config.vm.provision "cfengine" do |cf|
76
+ # cf.policy_server_address = "10.0.2.15"
77
+ # end
78
+
79
+ # Enable provisioning with Puppet stand alone. Puppet manifests
80
+ # are contained in a directory path relative to this Vagrantfile.
81
+ # You will need to create the manifests directory and a manifest in
82
+ # the file default.pp in the manifests_path directory.
83
+ #
84
+ # config.vm.provision "puppet" do |puppet|
85
+ # puppet.manifests_path = "manifests"
86
+ # puppet.manifest_file = "site.pp"
87
+ # end
88
+
89
+ # Enable provisioning with chef solo, specifying a cookbooks path, roles
90
+ # path, and data_bags path (all relative to this Vagrantfile), and adding
91
+ # some recipes and/or roles.
92
+ #
93
+ # config.vm.provision "chef_solo" do |chef|
94
+ # chef.cookbooks_path = "../my-recipes/cookbooks"
95
+ # chef.roles_path = "../my-recipes/roles"
96
+ # chef.data_bags_path = "../my-recipes/data_bags"
97
+ # chef.add_recipe "mysql"
98
+ # chef.add_role "web"
99
+ #
100
+ # # You may also specify custom JSON attributes:
101
+ # chef.json = { mysql_password: "foo" }
102
+ # end
103
+
104
+ # Enable provisioning with chef server, specifying the chef server URL,
105
+ # and the path to the validation key (relative to this Vagrantfile).
106
+ #
107
+ # The Opscode Platform uses HTTPS. Substitute your organization for
108
+ # ORGNAME in the URL and validation key.
109
+ #
110
+ # If you have your own Chef Server, use the appropriate URL, which may be
111
+ # HTTP instead of HTTPS depending on your configuration. Also change the
112
+ # validation key to validation.pem.
113
+ #
114
+ # config.vm.provision "chef_client" do |chef|
115
+ # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME"
116
+ # chef.validation_key_path = "ORGNAME-validator.pem"
117
+ # end
118
+ #
119
+ # If you're using the Opscode platform, your validator client is
120
+ # ORGNAME-validator, replacing ORGNAME with your organization name.
121
+ #
122
+ # If you have your own Chef Server, the default validation client name is
123
+ # chef-validator, unless you changed the configuration.
124
+ #
125
+ # chef.validation_client_name = "ORGNAME-validator"
126
+ end