Upload
jpalley
View
3.846
Download
2
Tags:
Embed Size (px)
DESCRIPTION
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. Its time to refactor. This talk will share our experience, results and best practices in splitting a single rails “application-system” into 30 independently maintainable yet interconnected applications. After two and a half years of development (starting in pre-Rails 1.0 days!), our live-trainer English learning system now supported multiple roles (learner/trainer/trainer supervisor/sales/materials creation/support/etc) and an exhaustive list of features to support our complex business processes. We set ourselves a year-long goal of splitting this monolithic system into small cooperating applications that could be developed independently by individual developers. At the same time, we could not lose the usability cohesiveness and data-interdependence that defined the power of our system. Through numerous iterations, many mistakes and a bit of pure-luck we developed an optimized process for the refactor and best practices for making 30 independent rails apps behave as one. The results: lower development time, greater stability and scalability and much higher developer happiness. We’ll talk about specific code, measurements, pitfalls, plugins, process and best practices to answer questions such as: How to know where to split single applications into many. How to measure the result. How the applications should interact with each other. How to reduce administration and DRY configuration applications. How to share data among applications. How to DRY for common logic. How to make a consistent user experience. How to interact with non-Ruby technology; in our case Erlang, FreeSWITCH (VoIP) and Flex
Citation preview
From 1 To 30How To Disassemble
One Monster App Into An Ecosystem Of 30
Jonathan Palley, CTO/COO
Guo Lei, Chief Architect
© 2010 Idapted, Ltd.
An
Experience
A Tale of Two Buildings
2009 ShanghaiLotus Riverside Community
1909 BeijingForbidden City
1
30
What is one?
The entire web application/system/platform
runs as one single rails application
(We are talking about really large systems. Multiple different types of clients/functions)
Problems
Hard to test/extend/scale
Confused new staff
What is 30?
A ecosystem of applications
Independent
Linked and Seamless
Basic features of each app• Separate database• Runs independently (complete story) • Lightweight (single developer)• Tight internal cohesion and loose external coupling
Advantages• Independent Development Cycle• Developer autonomy• Technology (im)maturity safety
APPEAL TO DEVELOPER LAZINESS
What’s the mystery of the forbidden city?
Consistent UI
• Shared CSS/JS/Styleguide• Common Helpers in Shared Gem• Safely try new things
All applications use the same base CSS/JS
Keep all the application the same style
<%= idp_include_js_css %># =><script src ="/assets/javascripts/frame.js" type="text/javascript"></script><link href="/assets/stylesheets/frame.css" media="screen" rel="stylesheet"
type="text/css" />
interface
CSS Frameworkinterface
Abstract Common Helpers to Gem
Search function for models
interface
Common Helpers: Combo search (cont)
View:<%= search_form_for(HistoryRecord, :interaction_id, :released,[:rating, {:collection=>assess_ratings}],[:mark_spot_num,{:range=>true}], [:created_at, {:ampm=>true}]) %>
Controller:@history_records = HistoryRecord.combo_search(params)
interface
Common Helpers: List table
well formattedwith pagination
sortablecustomizable
interface
Common Helpers: List table (cont)<%= idp_table_for(@history_records,:sortable=>true,:customize => "history_records") do |item, col| col.add :id, link_to(item.id, admin_history_record_path(item)),:order=>:id col.build :duration, :waiting_time, :review_time col.add :scenario, item.scenario_title, :order => :scenario_title col.add :mark_spot_num end%>
interface
Development Lifecycle
interface
1. Implement new View code/plugin in a second application
2. Abstract into plugin using existing “idp” helpers
3. Put it into main view gem
interface data
How do applications share data?(remember: each app has its own data)
data
-“Read Only” Database Connections- Services- AJAX Loaded View Segments
Business example
user
course
purchase
learning process
data
Purchase App
Requirement: List course packages for user to select to purchase
The course package data is stored in the “course” application
but
data
Solution
readonly db connection
data
course
Code
Model:class CoursePackage < ActiveRecord::Base acts_as_readonly :courseend
View:<ul><% CoursePackage.all.each do |package| %> <li><%= package.title %> <%= package.price %></li><% end %></ul>
data
Why doesn’t this break the rule of loose coupling?
data
acts_as_readonly in Depth
def acts_as_readonly(name, options = {})config = CoreService.app(name).databaseestablish_connection config[Rails.env]set_table_name(self.connection.current_database + (options[:table_name]||table_name).to_s)
end
data
acts_as_readonly in Depth
def acts_as_readonly(name, options = {})
config = CoreService.app(name).databaseestablish_connection config[Rails.env]set_table_name(self.connection.current_database + (options[:table_name]||table_name).to_s)
end
data
Core service
class CoreService < ActiveResource::Base self.site = :userdef self.app(app_name)
CoreService.find(app_name)end
end
data
Centralized configurationdata
How does Core know all the configurations?
Each app posts its configuration to core when it is started
data
data
config/site_config.ymlapp: courseapi: course_list: package/courses
config/initializers/idp_initializer.rb
CoreService.reset_config
data
core_service.rb in idp_lib
APP_CONFIG = YAML.load(Rails.root.join(“config/site_config.yml”))
def reset_config self.post(:reset_config, :app => { :name => APP_CONFIG["app"], :settings => APP_CONFIG, :database => YAML.load_file(
Rails.root.join("config/database.yml"))})end
data
Again, implemented in gem
data
config/environment.rb
config.gem ‘idp_helpers’config.gem ‘idp_lib’
data
gems
• Web services for “write” interactions
class CoursePackageService < ActiveSupport::Base self.site = :courseend
data
example
Roadmap needs to be generated after learner pays.
data
codesCourse: app/controllers/roadmap_services_controller.rb
def createRoadmap.generate(params[:user_id], params[:course_id])
end
Purchase: app/models/roadmap_service.rb
class RoadmapService < ActiveSupport::Base self.site = :courseendPurchase: app/models/order.rb
def activate_roadmap RoadmapService.create(self.user_id, self.course_id)end
data
AJAX Loaded Composite Viewdata
<div><%= ajax_load(url_of(:course, :course_list)) %></div>
Fetched from different
applications
Ecosystem url_for
interface data user
Features of User Service
• Registration/login• Profile management• Role Based Access Control
user
Access Control
Each Controller is one Node
user
* Posted to user service when app starts
Access Control
before_filter :check_access_right
def check_access_right unless xml_request? or inner_request? access_denied unless has_page_right?(params[:controller]) endend
user
* Design your apps so access control can be by controller!
How to share?
user
Step 1: User Authuser
Step 1: User Auth
config/initializers/idp_initializer.rb
ActionController::Base.session_store = :active_record_storeActiveRecord::SessionStore::Session.acts_as_remote :user,
:readonly => false
user
Step 2: Access ControlTell core its controllers structure
CoreService. reset_rights
def self.reset_rights data = load_controller_structure
self.post(:reset_rights, :data => data)end
user
Step 2: Access Control
before_filter :check_access_right
def check_access_right unless xml_request? or inner_request? access_denied unless has_page_right?(params[:controller]) endend
user
Step 2: Access Control
has_page_right?
Readonly db conn again
user
Step 2: Access Control
def has_page_right?(page) roles = current_user.roles
roles_of_page = IdpRoleRight.all(:conditions => ["path = ?", page]).map(&:role_id)
(roles - (roles - roles_of_page)).size > 0end
class IdpRoleRight < ActiveRecord::Base acts_as_readonly :user, :table_name => "role_rights"end
user
Again, gems!user
config/environment.rb
config.gem ‘idp_helpers’config.gem ‘idp_lib’config.gem ‘idp_core’
interface servicedata user
Support applications
• File• Mail• Comet service
service
Fileclass Article < ActiveRecord::Base has_filesend
@article.files.first.url
Upload File in Background to
FileService
Store with app_name,
model_name, model_id
Use readonly magic to easily
display
Idp_file_form
Specify Class that Has Files
Cometservice
class ChatRoom < ActiveRecord::Base acts_as_realtime end
<%= realtime_for(@chat_room, current_user.login) %>
<%= realtime_data(dom_id, :add, :top) %>
@chat_room.realtime_channel.broadcast(“hi world", current_user.login)
Host all in one domain
Load each rails app into a subdir, we use Unicorn
unicorn_rails --path /user unicorn_rails --path /studycenter unicorn_rails --path /scenario
Host all in one domain
use Nginx as a reverse proxy
location /user { proxy_pass http://rails_app_user; }
location /studycenter { proxy_pass http://rails_app_studycenter; }
Host all in one domain
All you see is a uniform URL
www.eqenglish.com/user www.eqenglish.com/studycenter www.eqenglish.com/scenario
Pair-deploy
How to split one into many?
By Story
Each App is one group of similar features.
By DataEach App writes to the same data
Example
• User Management• Course package• Purchase• Learning process• …
Iteration
Be adventurous at the beginning.Split one into as many as you think
is sensitive
Then you may find
• Some applications interact with each other frequently.
• Lots of messy and low efficiency code to deal with interacting.
Merge them into one.
Measurement
• Critical and core task of single app should not call services of others.
• One doesn’t need to know much about others’ business to do one task (or develop).
• Independent Stories
Pitfalls
• Applications need to be on the same intranet.
• No “right place” for certain cases.
Results: Higher Productivity
-Faster build to deploy-More developer autonomy-Safer- Scalable-Easier to “jump in”- Greater Happiness
© 2010 Idapted, Ltd.
Thank you!
Q&Ahttp://www.idapted.comhttp://developer.idapted.comhttp://t.sina.com.cn/[email protected] (@jpalley)[email protected] (@fiyuer)