Stargate
Stargate is a portal between ruby (and in the future not any) apps. It facilitates creating internal micro-services and allows to scale your logic pretty much infinitely.
Motivation & Foreword
Writing micro-services comes with huge overhead and problems on all ends:
- Services often speak pseudo REST interfaces, too complex and too troublesome.
- Consumers need client libraries that are either generated boilerplate code or crated libraries that break more often then they actually work.
- Testing on both ends is complicated and not really accurate. Often you still have to test your client libs too. Sometimes even generators that create your client libraries must be tested.
- Client libraries add layer of abstraction that's completely unnecessary and introduces confusion. Different services speak through different REST "practices". The HTTP verbs and headers holy war keeps going, no peace.
IMHO simple RPC services are hero solving this problems. They come with different ones, though, JSON-RPC being the best example.
Here are key objectives I took for this project:
- Simple protocol and exchange format for the start: HTTP + JSON. Minimum effort.
- Replaceable protocols and exchange formats in the long run.
- Server versioning out of the box.
- No REST practices.
- Client code meta-generated from server definitions. One client for all.
- Client forced to respect the versioning system.
- Easy for testing.
Finally, the main objective to call remote methods and obtain remote data just as you would call the method locally. Real key for me was to create a system where it doesn't matter if method or class are executed locally or remotely. Fire, get results, forget. No thinking of what's under the hood. The essence of distributed computing without affecting principles of our programming language.
Installation
Add this line to your application's Gemfile:
gem 'stargate', '0.1.3'
And then execute:
$ bundle
Or install it yourself as:
$ gem install stargate --version 0.1.3
Usage
Define your service:
module NightsWatch class NewBrother def self.oath "I pledge my life and honor to the Night's Watch, for this night and all the nights to come." end def self.call(name) Brother.new(name) end end class Brother attr_accessor :name def initialize(name) @name = name end def knowledge if name == "Jon Snow" "nothing" else "something" end end end end
Plain ruby object. Class methods are your entry points.
NOTE: Class method should return either basic type (String
, Numeric
, Float
, TrueClass
, FalseClass
, NilClass
), Hash
of basic types, object of our class or Array
of basics or such objects. That's first restriction - if you ask me I already write compliant code like this for years.
Put it on the wire! Add
config.ru
like this one:require 'stargate/server' require 'stargate/server/transport/sinatra' class NightsWatch # *snip* end registry = Stargate::Server::Registry.new do version 1 do serve NightsWatch::NewBrother do class_methods :call, :oath end serve NightsWatch::Brother do class_methods :create, :update attributes :name readers :knowledge end end end run Stargate::Server::Transport::Sinatra.new(registry)
Entry points are our class methods that shall be exposed over the network. They are remotely callable. The accessors
are simple all assessor attributes that should be exposed in case of returning object of that class. Finally readers
are read only attributes. Final note, the as
option of serve
block defines exposed name of the class.
NOTE: Here comes second restriction. You can expose only class methods to be callable. Instances are simply treated as data structures. If you expose message, it's result will be cached at the moment of serialization and only result passed over the wire. How do I call instance methods then? You don't.
Q: So how can I call ActiveRecord::Base#save
on model object?
A: Forget model objects, what you get is eventually a data structure. The structure is remotely stateless.
Q: What does it mean "remotely stateless"?
A: It means that you can make local changes to the object, but persistence of those changes must go through a service, either local or remote. Persistence methods like #save
should be called only at the end of the food chain, in the
final service.
Q: Show me an example?
class Book < ActiveRecord::Base
# *snip*
end
class CreateBook
def self.call(book)
unless book.valid?
raise "Uups, the book info is not complete!"
end
book.save
end
end
Q: What? It looks like Rails controller. Why I can't just call save directly? A: Don't think in the space of operations. Think in processes and define them clearly. Yes it's extra code initially. No it's not a boilerplate. This way you can easily test, mock and track your business processes.
Set up your client code:
require 'stargate/client' module NightsWatch end require_remote 'http+json://localhost:9292/v1' class NightsWatch::Brother def knowledge_description "#{name} knows #{knowledge}" end def hello "Hello, I'm #{name}!" end end jon = NightsWatch::NewBrother.call('Jon Snow') puts jon.hello puts jon.knowledge_description puts jon.class.name puts '----' sam = NightsWatch::NewBrother.call('Samwell Tarly') puts sam.knowledge_description puts '----' puts NightsWatch::NewBrother.oath
As you can see, single client handles everything. Also the way you connect to remote service is pretty much like requiring any other file or library. Client automatically fetches definitions of exposed classes from remote location (this is done once and can be easily cached for the eventuality of service downtime) and injects all registered classes into global namespace. Now injected classes behave pretty much like they'd be defined locally, with the only exception that under the hood they call remote API to execute stuff and obtain results. As you can also see in NightsWatch::Brother
proxy class, we can easily extend these classes with plain ruby code.
NOTE: One probably unsupported or at least troublesome thing at the moment is inheritance from the proxy class.
That's it! The services are stateless and can be trivially load balanced or put behind the proxy. The client is one and only and doesn't require you to write more than few lines of code. No custom stuff to learn or define before making use of remote services.
Check examples
directory to try these examples out.
Under the hood
The mechanism is uber simple. Expose two endpoints (with whatever protocol you prefer) on the server side without affecting original code. In case of HTTP transport that would be:
GET /v{version}/definitions.json
POST /v{version}/{klass_name}.{method}
First returns JSON (or however encoded) definitions of exposed classes. Later is an entry point for calling class methods of exposed classes. It takes encoded arguments as POST request body.
That's it for the server!
Now the client at require_remote
time fetches definitions and injects configured classes inheriting from Stargate::Client::Proxy
. All exposed methods are defined dynamically and they pass through to remote calls.
Development
You have two options to work with this project. The docker flow is suggested since solves problems of compatibility of tools.
Manual Setup
First off, make sure you have Ruby 2.2+ and latest version of Bundler on your machine. After checking out the repo, you can install dependencies and prepare the project with:
$ bin/setup
Now you can run tests:
$ bundle exec rake spec
You can also connect to interactive prompt that will allow you to experiment. To do this, run:
$ bundle exec bin/console
To install this gem onto your local machine, run:
$ bundle exec rake install
Setup with Docker
If you're lazy and don't wanna get into how the setup works, here's something for you. This project comes fully dockerized. Install docker toolchain and then go for:
$ docker-compose build
All done, you can do testing and fiddling around:
$ docker-compose run facebook_integration bash
root@xyyyyxx:/usr/local/src/rake-bump# bundle exec rake spec
root@xyyyyxx:/usr/local/src/rake-bump# bundle exec bin/console
Releasing
TODO: Add link to rake-bump release instructions...
Contributing
Bug reports and pull requests are welcome here.