eco_apps is a ‘client’ gem for rails applications to interact with each other in a rails application ecosystem. This is based on how we construct the Idapted platform and was first publicly presented at RailsConf (view ppt here: www.slideshare.net/jpalley/railsconf-2010-from-1-to-30-how-to-refactor-one-monolithic-application-into-an-application-ecosystem)
This gem will post an app’s configuration to the ‘master’ app (which is powered by the eco_apps_master gem) and provide functionality to power the ‘ecosystem’ (like web services, read only database connections and url helper method).
Git/Issues: http://github.com/eleutian/eco_apps Mailing List: http://groups.google.com/group/eco_apps Blog: http://developer.idapted.com
gem install eco_apps gem install eco_apps_master
As your business grows bigger, you just can’t stop adding new models/controllers to your original rails application – resulting in a messy, unmaintainable and difficult to deploy monolithic application. By splitting a single rails “application-system” into many independently maintainable yet interconnected applications, we’ve found a number of advantages: lower development time, greater stability and scalability and much higher developer happiness.
In this rails application ecosystem, there’s one application playing the master role. It manages the configuration info for all the applications. The ‘node’ application keeps its configuration in one file and post this to the master app when the server starts. The node app will ask the master app for another node’s info when they interact with each other. All these process are packaged in gems eco_apps_master and eco_apps.
eco_apps_master is used by master application. Any application using this gem will become the master app. eco-apps is used by all of the node applications. When an app uses this gem, it will be in the ecosystem.
Suppose we are going to build an online petstore. We’ll split the features into two groups or user stories. One group is for pet info management (adding pets from an admin view and browsing/commenting/sharing/etc. from an end-user view) and the second story is ordering (monitoring orders from an admin view and actually placing the order with necessary info from an end user view). We are going to use two rails application to accomplish this: ‘pet’ and ‘order’
Create the two rails application and add the necessary models/controllers. For example:
pet:
rails new pet_app cd pet_app rails g model dog rails g controller dogs
order:
rails new order_app cd order_app rails g model order rails g controller orders
It’s easy and normal to add functions for pet application - it doesn’t need to know anything about the order application. However, we run into a problem when creating order: an order needs to know the information about the dog model (in this case) that is being ordered.
Suppose the default page of the order application lists all of the dogs available for a user to select. In this case, the order application needs the data stored in the dog table of the pet application. Thus, we use a “read only database connection”, so that the dogs info can be read from (but not write to) the dog table. It is important to understand, the one application never writes to multiple databases.
To set up this read-only db connection, the order app needs to know the db config of the pet app. This is where the ‘master’ app comes in. It knows the configuration info for all of the applications and the node applications can get this information from the ‘master’ app.
So, let’s make the pet app as our master application:
# pet/Gemfile config.gem 'eco_apps_master', '>= 0.2.0'
When you restart server, you can see that it will create a table called “eco_apps_stores” which will store all of the apps’ info and a file “config/app_config.yml” which will store configuration of pet. And as ‘eco_apps_master’ is built on ‘eco_apps’, pet will also be one node of ecosystem.
Start pet at port 3000 in production mode and an exception will be raised, saying “master_app_url ‘production.lan’ is unreachable! Please change it in GEM_DIR/eco_apps/lib/platform_config.yml or APP_ROOT/config/app_config.yml and make sure the master app starts at this address.”.
“production.lan” is the default value of master_app_url in ‘GEM_DIR/eco_apps/lib/platform_config.yml’. In this example, we should change it to “localhost:3000”.
And as pet acts as master app here, we need to add ‘in_master_app: true’.
# pet/config/app_config.yml in_master_app: true url: http://localhost:3000 master_app_url: http://localhost:3000
Now we can add order app into this ecosystem as a node.
# order/Gemfile gem 'eco_apps', '>= 0.2.0'
Start order at port 3001, and edit config file.
# order/config/app_config.yml url: http://localhost:3001 master_app_url: http://localhost:3000
NOTE: If master_app_url is set in ‘GEM_DIR/eco_apps/lib/platform_config.yml’, it will works for all node apps.
Start pet first, and then order, you can see that order is posting its info to master(pet here) app.
NOTE: To make the requests respond faster in development mode, the app’s config info will be posted to master app only in production mode. As a result, you need to start one app in production mode at least 1 time to make it available to others.
Now we can make use of the gem’s magic to make it easy to setup a readonly db connection.
# order/app/models/dog.rb class Dog < ActiveRecord::Base acts_as_readonly :pet end class Order < ActiveRecord::Base belongs_to :dog end
That’s it. Now you can use the dog model just as normal active_record object - the only difference being that it reads the data from the pet’s database and it can not modify records. Note that acts_as_readonly relies on ActiveRecord::Base.connection.current_database, so the databases like sqlite3 not implementing this method are not supported.
It’s common that users will need to jump from one application or user story to another. In our petstore example, consider a functionality that links an order page to a detailed info page on that dog being ordered.
It’s easy to hard code the page url in order app, like
<%= link_to "view detail", "http://localhost:3001/dogs/#{dog.id}" %>
However, this will cause tight coupling between apps and become painful if URL’s change.
To solve this problem, applications can publicly expose URLs to other apps. Consider this configuration in the pet app:
# pet/config/app_config.yml api: url: dog_detail: dogs/:id
And this url_of code in the orders app:
# order/orders/index.html.erb <%= link_to "view detail", url_of(:pet, :dog_detail, :id => dog.id) %>
Sometimes you cannot avoid one app needed to update the information in another application’s database. This usually goes along with some business logic. We use active resource to achieve this and again, the configuration is automatic:
# order/models/dog_service.rb class DogService < ActiveRecourse::Base self.site = :pet end # pet/controllers/dog_services_controller.rb class DogServicesController < ActionController::Base ip_limited_access # optional, only can be accessed by intranet ip (defined in GEM_DIR/eco_apps/lib/platform_config.yml) end
Readonly models are not readonly in test mode. You can test them just as normal models. The difference is that they are in other databases.
If you would like to keep all the tests in the same database, you can define the columns of the readonly model in app_config.yml, then the tables will be created accordingly.
# order/config/app_config.yml readonly_for_test: dogs: string: breed, name
There’s two ways to set master_app_url. One is to set it in each app like this:
# config/app_config.yml master_app_url: http://internal_path_to_master.lan
As the master_app_url for all your apps in the same ecosystem (and usually server) is the same, it is often easier to set this configuration on the gem level:
# GEM_DIR/eco_apps/lib/platform_config.yml master_app_url: http://internal_path_to_master.lan
It is necessary to keep different url configuration for production and development mode, you can do this as following:
# APP_ROOT/config/app_config.yml url: development: http://example.dev production: http://example.production # GEM_DIR/eco_apps/lib/platform_config.yml master_app_url: development: http://example.dev production: http://example.production
How to make app using another’s configuration that is different from what stored in the master app? ¶ ↑
Sometimes it’s necessary for one application to use another’s configuration that is different from what stored in the master app. This may be because you need to use a different configuration on your own machine rather than on the test server, or even because the app doesn’t exist yet.
You can mock another’s configuration in development mode in this way:
# APP_ROOT/config/app_config.yml # other configuration cached_config: order: # This is another app's name url: # set url api: # The same rule with config in app_config.yml
In addition to this, the config of other apps will be cached in this way. If config of one app is changed, you need to delete it from ‘app_config.yml’ manually to keep it up-to-date.
We have a staging server and staging “master” app. This staging server holds a stable version of each app with real data. When we are working on an application on your local system, the ecosystem it connects to is this “stable staging” environment.
In the platform_config.yml file of the eco_apps gem you will find an intranet_ip setting. You can use this setting to define which intranet addresses are allowed to access the eco_app_master services. Set your firewall accordingly!