distribute_tree 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.gitignore +21 -0
  2. data/Gemfile +1 -0
  3. data/Guardfile +14 -0
  4. data/LICENSE +20 -0
  5. data/README.markdown +76 -0
  6. data/app/assets/javascripts/distribute_tree.js +137 -0
  7. data/app/controllers/distribute_controller.rb +105 -0
  8. data/app/models/distribute_tree_status.rb +16 -0
  9. data/app/views/distribute/servers.html.haml +38 -0
  10. data/config/routes.rb +12 -0
  11. data/distribute_tree.gemspec +32 -0
  12. data/lib/distribute_tree.rb +104 -0
  13. data/lib/rails_engine.rb +16 -0
  14. data/spec/dummy/.gitignore +15 -0
  15. data/spec/dummy/Gemfile +38 -0
  16. data/spec/dummy/README.rdoc +261 -0
  17. data/spec/dummy/Rakefile +7 -0
  18. data/spec/dummy/app/assets/images/rails.png +0 -0
  19. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  20. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  21. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  22. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  23. data/spec/dummy/app/mailers/.gitkeep +0 -0
  24. data/spec/dummy/app/models/.gitkeep +0 -0
  25. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  26. data/spec/dummy/config.ru +4 -0
  27. data/spec/dummy/config/application.rb +62 -0
  28. data/spec/dummy/config/boot.rb +6 -0
  29. data/spec/dummy/config/database.yml +25 -0
  30. data/spec/dummy/config/environment.rb +5 -0
  31. data/spec/dummy/config/environments/development.rb +37 -0
  32. data/spec/dummy/config/environments/production.rb +67 -0
  33. data/spec/dummy/config/environments/test.rb +37 -0
  34. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  35. data/spec/dummy/config/initializers/inflections.rb +15 -0
  36. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  37. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  38. data/spec/dummy/config/initializers/session_store.rb +8 -0
  39. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  40. data/spec/dummy/config/locales/en.yml +5 -0
  41. data/spec/dummy/config/routes.rb +58 -0
  42. data/spec/dummy/db/seeds.rb +7 -0
  43. data/spec/dummy/doc/README_FOR_APP +2 -0
  44. data/spec/dummy/lib/assets/.gitkeep +0 -0
  45. data/spec/dummy/lib/tasks/.gitkeep +0 -0
  46. data/spec/dummy/log/.gitkeep +0 -0
  47. data/spec/dummy/public/404.html +26 -0
  48. data/spec/dummy/public/422.html +26 -0
  49. data/spec/dummy/public/500.html +25 -0
  50. data/spec/dummy/public/favicon.ico +0 -0
  51. data/spec/dummy/public/index.html +241 -0
  52. data/spec/dummy/public/robots.txt +5 -0
  53. data/spec/dummy/script/rails +6 -0
  54. data/spec/dummy/test/fixtures/.gitkeep +0 -0
  55. data/spec/dummy/test/functional/.gitkeep +0 -0
  56. data/spec/dummy/test/integration/.gitkeep +0 -0
  57. data/spec/dummy/test/performance/browsing_test.rb +12 -0
  58. data/spec/dummy/test/test_helper.rb +13 -0
  59. data/spec/dummy/test/unit/.gitkeep +0 -0
  60. data/spec/dummy/vendor/assets/javascripts/.gitkeep +0 -0
  61. data/spec/dummy/vendor/assets/stylesheets/.gitkeep +0 -0
  62. data/spec/dummy/vendor/plugins/.gitkeep +0 -0
  63. data/spec/spec_helper.rb +39 -0
  64. data/spec/support/mongoid.rb +5 -0
  65. metadata +287 -0
@@ -0,0 +1,21 @@
1
+ *.rbc
2
+ *.sassc
3
+ .sass-cache
4
+ capybara-*.html
5
+ .rspec
6
+ .rvmrc
7
+ /.bundle
8
+ /vendor/bundle
9
+ /log/*
10
+ /tmp/*
11
+ /db/*.sqlite3
12
+ /public/system/*
13
+ /coverage/
14
+ /spec/tmp/*
15
+ **.orig
16
+ rerun.txt
17
+ pickle-email-*.html
18
+ .project
19
+ config/initializers/secret_token.rb
20
+
21
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ gemspec
@@ -0,0 +1,14 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :test do
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
6
+ watch(%r{^test/.+_test\.rb$})
7
+ watch('test/test_helper.rb') { "test" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/models/(.+)\.rb$}) { |m| "test/unit/#{m[1]}_test.rb" }
11
+ watch(%r{^app/controllers/(.+)\.rb$}) { |m| "test/functional/#{m[1]}_test.rb" }
12
+ watch(%r{^app/views/.+\.rb$}) { "test/integration" }
13
+ watch('app/controllers/application_controller.rb') { ["test/functional", "test/integration"] }
14
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 SunshineLibrary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ 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, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,76 @@
1
+ distribute_tree
2
+ ===============
3
+ 用于实现单个Cloud和多个Local服务器之间数据共享的 Rails Engine 。
4
+
5
+ Cloud(1) <-> Local(n) 同步机制概述
6
+ ---------------
7
+ 同步模式:
8
+
9
+ * 自动: 保存一个资源均只同步自身的JSON和File,即不递归同步子集。
10
+ * 手动: 新增一个学校,或者只想给某些学校,分配某些资源,在后台管理点击同步,并递归子集。
11
+
12
+ 启动方式
13
+ ---------------
14
+
15
+ ```bash
16
+ RAILS_ENV=production bundle exec rake resque:work QUEUE='cloud_distribute_tree' --trace
17
+ ```
18
+
19
+ 如果需要对一个queue起多个work,那么多开几个rake进程就好了,系统会自动排队和并发处理的。
20
+
21
+ [循环sync] local(只要在这里把资源标上创建地点即可让cloud识别) -> cloud -> locals
22
+
23
+ 同步架构变迁历史概述
24
+ ---------------
25
+ #### 第一代(mysql replication)
26
+
27
+ 优点:
28
+
29
+ 1. 从数据库层面一致。
30
+
31
+ 缺点:
32
+
33
+ 1. 单向。
34
+ 1. 无法提供细力度控制和外部操作。
35
+
36
+ #### 第二代(rabbitmq)
37
+
38
+ 优点:
39
+
40
+ 1. RabbitMQ高性能
41
+ 2. 引入基于JSON格式的API在不同系统间同步,并分成元数据和File两种数据类型分别分发。
42
+
43
+ 缺点:
44
+
45
+ 1. 按订阅全部同步,容易造成VPN网络堵塞。虽然可以改成点对点发送,但也失去了用rabbitmq的必要。
46
+ 2. 无细力度的UI,不透明。这个开发难度比 resque 高,且是另外一套系统。
47
+ 3. 无法管理细力度的失败的同步。
48
+
49
+ #### 第三代(resque)
50
+ 架构:基于resque队列系统。队列可视化,rails风格。从优点上继承了上代的RabbitMQ架构。
51
+
52
+ A. 初始化同步,或者中途中断后同步, cloud -> local。
53
+
54
+ 配置学校,可以按school同步,并在resque里递归从属或关联子集,产生新队列(这一想法基于之前给刘聪做的递归更新父级时间戳 [mongoid_touch_parents_recursively](https://github.com/SunshineLibrary/mongoid_touch_parents_recursively) 。
55
+
56
+ B. 增量更新同步. cloud -> local (lesson) ,或者local -> cloud (piece) 。
57
+
58
+ 在数据同步后紧接着触发文件同步,都放在resque里。两者交互机制就是经典的client-server API 架构CRUD数据,这样也就符合了我来书屋面试前还没成型的想法,隐约地想要把学校local也看成是cloud的伪android客户端。删除也参考warpgate的做法。
59
+
60
+ 注: 感谢和小平的冲突,感谢和诺哥的讨论,在萨莉亚吃完晚饭后,萦绕的思绪突然激发了这一想法。这个完全不是时间逼的。
61
+
62
+ FAQ
63
+ ---------------
64
+ 问题: paperclip包含的静态资源过大,比如100M,在cloud的resque连接上local的Rails接口去上传文件时会发生网络请求超时错误,并会占用一个unicorn slave。
65
+
66
+ 答案: 可以反过来做,改为在元数据同步过去到local后,不管paperclip包含的文件对象大小,
67
+ 让local自己主动向cloud的nginx静态资源服务器读取大文件,并在下载完成后保存即可,从而达到异步的效果。
68
+ 这点warpgate就是这样做的,跨网络传输文件还是下行快一点。
69
+
70
+ TODO
71
+ ---------------
72
+ 1. 突破Mongoid.relations限制以支持ActiveModel
73
+
74
+ 相关引用
75
+ ---------------
76
+ 1. [warpgate](https://github.com/SunshineLibrary/warpgate)
@@ -0,0 +1,137 @@
1
+ // 添加分发按钮到资源*表格*的每一项,并显示同步状态。
2
+ // =======================================
3
+ //
4
+ // 只在cloud开启该分发按钮
5
+ //
6
+ // =======================================
7
+ // ### 使用方法 ###
8
+ //
9
+ // 在table加上属性model_name,以及在各tr的首个td里加上uuid和name两个属性。
10
+ //
11
+ // %table.{model_name: :App}
12
+ // %tr
13
+ // %th.span5 UUID
14
+ // %th.span3= t "App.name"
15
+ // ...
16
+ // - @apps.each_with_index do |app, idx|
17
+ // %tr
18
+ // %td{uuid: app.uuid, name: app.name}= app.uuid
19
+ // ...
20
+ //
21
+ // TODO 载入resque是否正常服务
22
+ // TODO 加入验证用户
23
+ //
24
+ $(document).ready(function() {
25
+
26
+ var table = $("table[model_name]");
27
+ if (table.length === 0) { return false; } // 如果没有配置
28
+ if (table.length >= 2) { alert("目前distribute_tree.js只支持单页同步一种资源类型"); return false; }
29
+ var table_model_name = table.attr('model_name');
30
+
31
+ // Constant definement
32
+ var api_prefix = "/distribute_tree";
33
+
34
+ // 只有管理员才能同步学校
35
+ // TODO optimize
36
+ if (!$(".navbar ul.nav.pull-right li a").text().match(/陈大伟/) && (table_model_name === 'School')) { return false; }
37
+
38
+ // 1. 初始化页面元素
39
+ //
40
+ // 1.1 th 分发按钮
41
+ table.find('tr:first').append(
42
+ $("<th>").addClass("pointer span1").html(
43
+ $("<span>").addClass("icon icon-th")
44
+ )
45
+ // 并绑定 选择学校列表 弹框
46
+ ).on('click', 'th:last', function(event) {
47
+ var uuids = _.map(trs.find('td:last input:checked'), function(input) { return $(input).attr('uuid'); });
48
+ if (uuids.length === 0) { alert("请选择至少一个资源用于同步"); event.preventDefault(); return false; }
49
+ var modal_html = function() { return distribute_to_schools_modal.find('.modal'); };
50
+
51
+ function show() {
52
+ // 统计显示勾选的资源数目
53
+ modal_html().find('.modal-header h4.title span.count').html(uuids.length);
54
+ modal_html().modal('show');
55
+ }
56
+
57
+ // 判断是否ajax载入学校列表
58
+ if (_.isEmpty($("#distribute_to_schools_modal").html())) {
59
+ $.get(api_prefix + "/distribute/servers.html", function(html) {
60
+ distribute_to_schools_modal.html(html);
61
+
62
+ // 2. 绑定**提交事件**
63
+ distribute_to_schools_modal.on('click', 'button.submit', function() {
64
+ var school_uuids = _.map(distribute_to_schools_modal.find('table tr:gt(0) td input:checked'), function(input) { return $(input).attr('id'); });
65
+ if (school_uuids.length === 0) { alert("请选择至少一个学校用于同步"); event.preventDefault(); return false; }
66
+
67
+ // 2.1 处理元素显示
68
+ var btn = distribute_to_schools_modal.find(".modal-footer button.save-button");
69
+ btn.button('loading');
70
+
71
+ // 2.2 ajax提交数据
72
+ var data = {
73
+ model_name: table_model_name,
74
+ uuids: uuids,
75
+ school_uuids: school_uuids
76
+ };
77
+ // 2.3 服务器状态返回
78
+ $.post(api_prefix + '/distribute', data, function(e) {
79
+ var alert_html = modal_html().find(".alert");
80
+ alert_html.find('div.alert').removeClass('alert-success').removeClass('alert-error').addClass('alert-' + e.status);
81
+ alert_html.find('#flash_notice span.status').html('已经同步' + uuids.length + '个资源到' + school_uuids.length + '个服务器。');
82
+ $(".container.content").prepend(alert_html);
83
+ }).always(function(e) {
84
+ modal_html().modal('hide');
85
+ btn.button('reset');
86
+ });
87
+ });
88
+ // 并浮现
89
+ show();
90
+ });
91
+ } else {
92
+ show();
93
+ }
94
+ });
95
+ // 1.2 td 是否分发该资源
96
+ var trs = table.find('tr:gt(0)');
97
+ var is_defined_uuid_and_name_attrs = false; // check setup
98
+ _.each(trs, function(_tr) {
99
+ var tr = $(_tr);
100
+ var td_first = tr.find('td[uuid]');
101
+ if (td_first.length !== 0) { is_defined_uuid_and_name_attrs = true; }
102
+ tr.append(
103
+ $("<td>").append(
104
+ $("<input>").attr("type", "checkbox")
105
+ .attr('uuid', td_first.attr('uuid'))
106
+ .attr('name', td_first.attr('name'))
107
+ )
108
+ );
109
+ });
110
+ if (!is_defined_uuid_and_name_attrs) {
111
+ alert("没有定义在每行记录定义uuid和name,请察看distribute_tree.js的文档。");
112
+ event.preventDefault(); return false;
113
+ }
114
+ // 1.3 放置一个 学校列表容器
115
+ var distribute_to_schools_modal = $("body").append(
116
+ $("<div>").attr("id", "distribute_to_schools_modal")
117
+ ).find('#distribute_to_schools_modal');
118
+
119
+ // 1.4 载入各个资源的同步状态
120
+ var params = {model_name: table_model_name, uuids: _.map(trs.find('td:last input'), function(input) { return $(input).attr('uuid'); })};
121
+ $.get(api_prefix + "/distribute.json", params, function(result) {
122
+ _.each(trs, function(_tr) {
123
+ var td_last = $(_tr).find('td:last');
124
+ var urls = result.data[td_last.find('input[type=checkbox]').attr('uuid')];
125
+ // 如果存在*没有分发完成*的记录
126
+ if (urls) {
127
+ td_last.append($("<div>").addClass('pointer server_urls').text(urls.length))
128
+ .append($("<div>").addClass('hide server_urls count').text(urls.join(', ')));
129
+ }
130
+ });
131
+ // 显示具体分发地址
132
+ trs.on('click', 'td:last .server_urls', function(e) {
133
+ $(e.target).closest('td').find('.server_urls.count').toggle();
134
+ });
135
+ });
136
+
137
+ });
@@ -0,0 +1,105 @@
1
+ # encoding: UTF-8
2
+ class DistributeController < ApplicationController
3
+
4
+ #######################
5
+ ##### A. creator ####
6
+ #######################
7
+
8
+ def create
9
+ error_messages = []
10
+ error_messages << "Missing model name" if params[:model_name].blank?
11
+ error_messages << "Missing uuid" if params[:uuids].blank?
12
+ error_messages << "Please select an school" if params[:school_uuids].blank?
13
+
14
+ if not error_messages.blank?
15
+ render json: {status: "error", messages: error_messages}, status: :bad_request; return false
16
+ end
17
+
18
+ server_urls = School.where(uuid: {"$in" => params[:school_uuids]}).map(&:server_url)
19
+
20
+ params[:uuids].to_a.each do |uuid|
21
+ item = params[:model_name].classify.safe_constantize.uuid(uuid)
22
+ item.distribute_with_children(server_urls)
23
+ end
24
+
25
+ render json: {status: "success", messages: ["已加入到发送队列"]}
26
+ end
27
+
28
+ def servers
29
+ @schools = School.asc(:_id).where(is_enable_sync: true)
30
+ render layout: false
31
+ end
32
+
33
+ def index
34
+ data = DistributeTreeStatus.where(
35
+ item_class: params[:model_name],
36
+ item_uuid: {"$in" => params[:uuids]},
37
+ ).to_a.inject({}) do |h, status|
38
+ h[status.item_uuid] ||= []
39
+ h[status.item_uuid] << status.server_url
40
+ h
41
+ end
42
+
43
+ render json: {status: 'success', data: data }
44
+ end
45
+
46
+ # TODO
47
+ def paperclip_file
48
+ params[:model_name].constantize
49
+ end
50
+
51
+ #######################
52
+ ##### B. receiver ####
53
+ #######################
54
+
55
+ def receive
56
+ payload = params[:payload]
57
+
58
+ unless payload and payload[:model_name]
59
+ render json: {status: 'error', message: "payload and model_name required"}
60
+ return
61
+ end
62
+
63
+ model = payload[:model_name].classify().constantize
64
+
65
+ uuid = nil
66
+ if payload[:model] and payload[:model].is_a?(Hash)
67
+ uuid = payload[:model][:uuid]
68
+ end
69
+
70
+ unless model and uuid
71
+ render json: {status: 'error', message: "model and uuid not identified"}
72
+ return
73
+ end
74
+
75
+ model = model.unscoped # 兼容被删除资源
76
+
77
+ if payload[:model][:delete]
78
+ model.where(uuid: uuid).destroy_all
79
+ render json: {status: 'success', message: "model deleted"}
80
+ return
81
+ end
82
+
83
+ if model.where(uuid: uuid).exists?
84
+ # update, skip validation
85
+ item = model.uuid(uuid)
86
+ elsif [Piece, Image].include? model
87
+ # Local不自动创建Piece和Image, 只更新老师添加的
88
+ render json: {status: 'success'}
89
+ return
90
+ else
91
+ item = model.new
92
+ end
93
+
94
+ payload[:model].delete '_id'
95
+
96
+ item.assign_attributes payload[:model], without_protection: true
97
+ item.deleted_at = nil if payload[:model][:deleted_at].blank? # 恢复删除。也许assign_attributes payload[:model]这里面已经包含了。
98
+ if item.save validate: false
99
+ render json: {status: 'success'}
100
+ else
101
+ render json: {status: 'error', message: "fail to create or update model"}
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: UTF-8
2
+
3
+ class DistributeTreeStatus
4
+ include Mongoid::Document; self.store_in database: self.name.underscore
5
+ include Mongoid::Timestamps
6
+
7
+ field :item_class, type: String
8
+ field :item_uuid, type: String
9
+ field :server_url, type: String
10
+
11
+ index({item_class: 1, item_uuid: 1, server_url: 1}, {background: true})
12
+
13
+ def self.insert item_class, item_uuid, server_url; DistributeTreeStatus.find_or_create_by(item_class: item_class, item_uuid: item_uuid, server_url: server_url) end
14
+ def self.delete item_class, item_uuid, server_url; DistributeTreeStatus.where(item_class: item_class, item_uuid: item_uuid, server_url: server_url).delete_all end
15
+
16
+ end
@@ -0,0 +1,38 @@
1
+ .modal.hide.fade{ "aria-hidden" => "true", :role => "dialog", :tabindex => "-1"}
2
+ %ul.items
3
+
4
+ .modal-header
5
+ %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} ×
6
+ %h4.title
7
+ %span 分发
8
+ %span.count
9
+ %span 个资源到下列勾选服务器
10
+
11
+ .modal-body
12
+ = form_tag({controller: "distribute", action: "create"}, method: "post", class: "form-horizontal") do
13
+ %div
14
+ %table.table.table-striped
15
+ %tbody
16
+ %tr
17
+ %th.span5.uuid.hide UUID
18
+ %th.span3 服务器
19
+ // %th.span2 服务器状态
20
+ %th.span2 分发
21
+ - @schools.each do |school|
22
+ %tr
23
+ %td.uuid.hide= school.uuid
24
+ %td= link_to school.name, school_path(school.uuid)
25
+ %td= check_box_tag(school.uuid)
26
+
27
+ .modal-footer
28
+ %button.submit.btn.btn-primary{:class => 'save-button', "data-loading-text" => "分发中…" } 分发
29
+ %button.btn{"aria-hidden" => "true", "data-dismiss" => "modal"} 取消
30
+
31
+ .alert.hide
32
+ %div.alert
33
+ %a.close{'data-dismiss' => 'alert'} x
34
+ %div#flash_notice
35
+ %span.status
36
+ %span 请前往
37
+ = link_to "同步队列", "/resque/queues/#{Mongoid::DistributeTree.default_queue}"
38
+ %span 察看最新状态
@@ -0,0 +1,12 @@
1
+ # encoding: UTF-8
2
+
3
+ Rails.application.routes.draw do
4
+
5
+ namespace :distribute_tree do
6
+ resources :distribute do
7
+ get :servers, :paperclip_file
8
+ post :receive
9
+ end
10
+ end
11
+
12
+ end