diff --git a/app/models/build_list.rb b/app/models/build_list.rb index 27a06e29b..313041373 100644 --- a/app/models/build_list.rb +++ b/app/models/build_list.rb @@ -290,6 +290,18 @@ class BuildList < ActiveRecord::Base BuildList.where(:id => extra_build_lists).where('status != ?', BUILD_PUBLISHED).count == 0 end + def human_average_build_time + I18n.t('layout.project_statistics.human_average_build_time', {:hours => (average_build_time/3600).to_i, :minutes => (average_build_time%3600/60).to_i}) + end + + def formatted_average_build_time + "%02d:%02d" % [average_build_time / 3600, average_build_time % 3600 / 60] + end + + def average_build_time + project.project_statistics.where(:arch_id => arch_id).first.try(:average_build_time) || 0 + end + def self.human_status(status) I18n.t("layout.build_lists.statuses.#{HUMAN_STATUSES[status]}") end diff --git a/app/models/build_list_observer.rb b/app/models/build_list_observer.rb index dc1921ecf..d9d4c0bc0 100644 --- a/app/models/build_list_observer.rb +++ b/app/models/build_list_observer.rb @@ -14,9 +14,14 @@ class BuildListObserver < ActiveRecord::Observer if record.status == BuildList::SUCCESS # Update project average build time - build_count = record.project.build_count - new_av_time = ( record.project.average_build_time * build_count + record.duration ) / ( build_count + 1 ) - record.project.update_attributes({ :average_build_time => new_av_time, :build_count => build_count + 1 }, :without_protection => true) + begin + statistic = record.project.project_statistics.find_or_create_by_arch_id(record.arch_id) + rescue ActiveRecord::RecordNotUnique + retry + end + build_count = statistic.build_count + new_av_time = ( statistic.average_build_time * build_count + record.duration ) / ( build_count + 1 ) + statistic.update_attributes(:average_build_time => new_av_time, :build_count => build_count + 1) end end end diff --git a/app/models/project.rb b/app/models/project.rb index 9c4ff4c63..64b9c776c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -15,6 +15,7 @@ class Project < ActiveRecord::Base has_many :project_to_repositories, :dependent => :destroy has_many :repositories, :through => :project_to_repositories has_many :project_tags, :dependent => :destroy + has_many :project_statistics, :dependent => :destroy has_many :build_lists, :dependent => :destroy has_many :hooks, :dependent => :destroy @@ -193,14 +194,6 @@ class Project < ActiveRecord::Base end end - def human_average_build_time - I18n.t("layout.projects.human_average_build_time", {:hours => (average_build_time/3600).to_i, :minutes => (average_build_time%3600/60).to_i}) - end - - def formatted_average_build_time - "%02d:%02d" % [average_build_time / 3600, average_build_time % 3600 / 60] - end - def destroy_project_from_repository(repository) AbfWorker::BuildListsPublishTaskManager.destroy_project_from_repository self, repository end diff --git a/app/models/project_statistic.rb b/app/models/project_statistic.rb new file mode 100644 index 000000000..717c5d8ad --- /dev/null +++ b/app/models/project_statistic.rb @@ -0,0 +1,10 @@ +class ProjectStatistic < ActiveRecord::Base + + belongs_to :arch + belongs_to :project + + validates :arch_id, :project_id, :average_build_time, :build_count, :presence => true + validates :project_id, :uniqueness => {:scope => :arch_id} + + attr_accessible :average_build_time, :build_count +end diff --git a/app/views/api/v1/projects/index.json.jbuilder b/app/views/api/v1/projects/index.json.jbuilder index 8bfd59136..b6d4296c8 100644 --- a/app/views/api/v1/projects/index.json.jbuilder +++ b/app/views/api/v1/projects/index.json.jbuilder @@ -1,6 +1,6 @@ json.projects @projects do |project| json.partial! 'project', :project => project - json.(project, :visibility, :description, :ancestry, :has_issues, :has_wiki, :default_branch, :is_package, :average_build_time, :publish_i686_into_x86_64) + json.(project, :visibility, :description, :ancestry, :has_issues, :has_wiki, :default_branch, :is_package, :publish_i686_into_x86_64) json.created_at project.created_at.to_i json.updated_at project.updated_at.to_i json.partial! 'api/v1/shared/owner', :owner => project.owner diff --git a/app/views/api/v1/projects/show.json.jbuilder b/app/views/api/v1/projects/show.json.jbuilder index 7f1f963c9..b820cf415 100644 --- a/app/views/api/v1/projects/show.json.jbuilder +++ b/app/views/api/v1/projects/show.json.jbuilder @@ -1,12 +1,17 @@ json.project do json.partial! 'project', :project => @project - json.(@project, :visibility, :description, :ancestry, :has_issues, :has_wiki, :default_branch, :is_package, :average_build_time, :publish_i686_into_x86_64) + json.(@project, :visibility, :description, :ancestry, :has_issues, :has_wiki, :default_branch, :is_package, :publish_i686_into_x86_64) json.created_at @project.created_at.to_i json.updated_at @project.updated_at.to_i json.partial! 'api/v1/shared/owner', :owner => @project.owner json.maintainer do json.partial! 'api/v1/shared/member', :member => @project.maintainer end + + json.project_statistics @project.project_statistics do |statistic| + json.(statistic, :average_build_time, :build_count, :arch_id) + end + json.repositories @project.repositories do |repo| json.(repo, :id, :name) json.url api_v1_repository_path(repo.name, :format => :json) diff --git a/app/views/projects/build_lists/_build_list.html.haml b/app/views/projects/build_lists/_build_list.html.haml index 40b8df982..1b9476e79 100644 --- a/app/views/projects/build_lists/_build_list.html.haml +++ b/app/views/projects/build_lists/_build_list.html.haml @@ -6,10 +6,10 @@ - if BuildList::HUMAN_STATUSES[build_list.status].in? [:build_pending, :build_started, :build_publish] %time.js-relative-date{:datetime => build_list.updated_at.strftime("%FT%T%:z"), :title => build_list.updated_at.strftime("%F %T")} = build_list.updated_at.strftime "%F %T" - - if build_list.build_started? && ((build_list.project.average_build_time || 0) > 0) + - if build_list.build_started? && (build_list.average_build_time > 0) \/ %time - = build_list.project.formatted_average_build_time + = build_list.formatted_average_build_time - if build_list.project.present? %td= link_to build_list.project.name_with_owner, build_list.project %td= build_list_version_link(build_list) diff --git a/app/views/projects/build_lists/show.html.haml b/app/views/projects/build_lists/show.html.haml index ca48b700b..956a18845 100644 --- a/app/views/projects/build_lists/show.html.haml +++ b/app/views/projects/build_lists/show.html.haml @@ -98,7 +98,7 @@ %br .leftlist .rightlist - = "#{@build_list.human_current_duration} / #{@build_list.project.human_average_build_time}" + = "#{@build_list.human_current_duration} / #{@build_list.human_average_build_time}" .both - if @build_list.can_cancel? && can?(:cancel, @build_list) diff --git a/config/application.yml.sample b/config/application.yml.sample index 10a879779..a718c32ff 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -9,6 +9,9 @@ common: &common - 127.0.0.100 abf_worker: publish_workers_count: 2 + log_server: + host: 127.0.0.1 + port: 6379 keys: key_pair_secret_key: 'key_pair_secret_key' airbrake_api_key: 'airbrake_api_key' diff --git a/config/application.yml.travis b/config/application.yml.travis index f10f585ec..6715379ef 100644 --- a/config/application.yml.travis +++ b/config/application.yml.travis @@ -9,6 +9,9 @@ common: &common - 127.0.0.100 abf_worker: publish_workers_count: 2 + log_server: + host: 127.0.0.1 + port: 6379 keys: key_pair_secret_key: 'key_pair_secret_key' airbrake_api_key: 'airbrake_api_key' diff --git a/config/locales/models/project.en.yml b/config/locales/models/project.en.yml index bf018c003..542571860 100644 --- a/config/locales/models/project.en.yml +++ b/config/locales/models/project.en.yml @@ -47,7 +47,6 @@ en: sections: Sections has_issue_description: Tracker adds a lightweight issue management system tightly integrated with your repository. has_wiki_description: Wikis are the simplest way to allow other users to contribute content. Any user can create and edit pages for documentation, examples, support or anything you wish. - human_average_build_time: Expected time is %{hours} h. %{minutes} min. git_global_setup: Git global setup create_repository: Create Repository move_files_to_folder: Move files you need to the project or create them. diff --git a/config/locales/models/project.ru.yml b/config/locales/models/project.ru.yml index 7f9cfc5fc..560f96151 100644 --- a/config/locales/models/project.ru.yml +++ b/config/locales/models/project.ru.yml @@ -3,7 +3,7 @@ ru: projects: branches: Ветки delete_branch: Удалить ветку - restore_branch: Востановить ветку + restore_branch: Восстановить ветку no_branches: Нет веток base_branch: Текущая ветка compare: Сравнить @@ -47,7 +47,6 @@ ru: sections: Разделы has_issue_description: Трэкер предоставляет лекговесный менеджер для задач по разработке Вашего проекта. has_wiki_description: Wiki - это самый простой способ предоставить другим вносить свой вклад в развитие Вашего проекта. Каждый пользователь нашего сервиса может использовать Wiki для документирования, примеров, поддержки или всего другого, в чем у Вас появится необходимость. - human_average_build_time: 'Ожидаемое время: %{hours} ч. %{minutes} мин.' git_global_setup: Общие настройки Git create_repository: Создание репозитория move_files_to_folder: Переместите нужные файлы в проект или создайте их. diff --git a/config/locales/models/project_statistic.en.yml b/config/locales/models/project_statistic.en.yml new file mode 100644 index 000000000..aff286d3e --- /dev/null +++ b/config/locales/models/project_statistic.en.yml @@ -0,0 +1,14 @@ +en: + layout: + project_statistics: + human_average_build_time: Expected time is %{hours} h. %{minutes} min. + + flash: + project_statistic: + + + activerecord: + models: + project_statistic: Project statistic + attributes: + project_statistic: \ No newline at end of file diff --git a/config/locales/models/project_statistic.ru.yml b/config/locales/models/project_statistic.ru.yml new file mode 100644 index 000000000..71e41b735 --- /dev/null +++ b/config/locales/models/project_statistic.ru.yml @@ -0,0 +1,16 @@ +ru: + layout: + project_statistics: + human_average_build_time: 'Ожидаемое время: %{hours} ч. %{minutes} мин.' + + flash: + project_statistic: + + + activerecord: + models: + project_statistic: Статистика проекта + attributes: + project_statistic: + + diff --git a/db/migrate/20130731120901_create_project_statistics.rb b/db/migrate/20130731120901_create_project_statistics.rb new file mode 100644 index 000000000..3d7103210 --- /dev/null +++ b/db/migrate/20130731120901_create_project_statistics.rb @@ -0,0 +1,13 @@ +class CreateProjectStatistics < ActiveRecord::Migration + def change + create_table :project_statistics do |t| + t.integer :average_build_time, :null => false, :default => 0 + t.integer :build_count, :null => false, :default => 0 + t.integer :arch_id, :null => false + t.integer :project_id, :null => false + + t.timestamps + end + add_index :project_statistics, [:project_id, :arch_id], :unique => true + end +end diff --git a/db/migrate/20130731130518_drop_average_build_time_and_build_count_from_project.rb b/db/migrate/20130731130518_drop_average_build_time_and_build_count_from_project.rb new file mode 100644 index 000000000..723d331aa --- /dev/null +++ b/db/migrate/20130731130518_drop_average_build_time_and_build_count_from_project.rb @@ -0,0 +1,11 @@ +class DropAverageBuildTimeAndBuildCountFromProject < ActiveRecord::Migration + def up + remove_column :projects, :average_build_time + remove_column :projects, :build_count + end + + def down + add_column :projects, :average_build_time, :integer, :null => false, :default => 0 + add_column :projects, :build_count, :integer, :null => false, :default => 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index b1cd1d2d7..67a46f31b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20130724105821) do +ActiveRecord::Schema.define(:version => 20130731130518) do create_table "activity_feeds", :force => true do |t| t.integer "user_id", :null => false @@ -376,6 +376,17 @@ ActiveRecord::Schema.define(:version => 20130724105821) do add_index "project_imports", ["platform_id", "name"], :name => "index_project_imports_on_name_and_platform_id", :unique => true, :case_sensitive => false + create_table "project_statistics", :force => true do |t| + t.integer "average_build_time", :default => 0, :null => false + t.integer "build_count", :default => 0, :null => false + t.integer "arch_id", :null => false + t.integer "project_id", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "project_statistics", ["project_id", "arch_id"], :name => "index_project_statistics_on_project_id_and_arch_id", :unique => true + create_table "project_tags", :force => true do |t| t.integer "project_id" t.string "commit_id" @@ -412,8 +423,6 @@ ActiveRecord::Schema.define(:version => 20130724105821) do t.boolean "has_wiki", :default => false t.string "default_branch", :default => "master" t.boolean "is_package", :default => true, :null => false - t.integer "average_build_time", :default => 0, :null => false - t.integer "build_count", :default => 0, :null => false t.integer "maintainer_id" t.boolean "publish_i686_into_x86_64", :default => false t.string "owner_uname", :null => false diff --git a/lib/abf_worker/model_helper.rb b/lib/abf_worker/model_helper.rb index 9d8de6653..ff9aefd9f 100644 --- a/lib/abf_worker/model_helper.rb +++ b/lib/abf_worker/model_helper.rb @@ -1,59 +1,70 @@ -module AbfWorker - module ModelHelper - # In model which contains this helper should be: - # - #abf_worker_args - # - #build_canceled - - def abf_worker_log - Resque.redis.get(service_queue) || I18n.t('layout.build_lists.log.not_available') - end - - def add_job_to_abf_worker_queue - Resque.push( - worker_queue_with_priority, - 'class' => worker_queue_class, - 'args' => [abf_worker_args] - ) - end - - def cancel_job - deleted = Resque::Job.destroy( - worker_queue_with_priority, - worker_queue_class, - abf_worker_args - ) - if deleted == 1 - build_canceled - else - send_stop_signal - end - true - end - - def worker_queue_with_priority(queue = nil) - queue ||= abf_worker_base_queue - queue << '_' << abf_worker_priority if abf_worker_priority.present? - queue - end - - def worker_queue_class(queue_class = nil) - queue_class ||= "AbfWorker::#{abf_worker_base_queue.classify}" - queue_class << abf_worker_priority.capitalize - end - - private - - def send_stop_signal - Resque.redis.setex( - "#{service_queue}::live-inspector", - 240, # Data will be removed from Redis after 240 sec. - 'USR1' # Immediately kill child but don't exit - ) - end - - def service_queue - "abfworker::#{abf_worker_base_queue.gsub(/\_/, '-')}-#{id}" - end +module AbfWorker::ModelHelper + # In model which contains this helper should be: + # - #abf_worker_args + # - #build_canceled + def self.included(base) + base.extend(ClassMethods) end + + module ClassMethods + def log_server + @log_server ||= Redis.new( + :host => APP_CONFIG['abf_worker']['log_server']['host'], + :port => APP_CONFIG['abf_worker']['log_server']['port'] + ) + end + end + + def abf_worker_log + self.class.log_server.get(service_queue) || I18n.t('layout.build_lists.log.not_available') + end + + def add_job_to_abf_worker_queue + Resque.push( + worker_queue_with_priority, + 'class' => worker_queue_class, + 'args' => [abf_worker_args] + ) + end + + def cancel_job + deleted = Resque::Job.destroy( + worker_queue_with_priority, + worker_queue_class, + abf_worker_args + ) + if deleted == 1 + build_canceled + else + send_stop_signal + end + true + end + + def worker_queue_with_priority(queue = nil) + queue ||= abf_worker_base_queue + queue << '_' << abf_worker_priority if abf_worker_priority.present? + queue + end + + def worker_queue_class(queue_class = nil) + queue_class ||= "AbfWorker::#{abf_worker_base_queue.classify}" + queue_class << abf_worker_priority.capitalize + end + + private + + def send_stop_signal + Resque.redis.setex( + "#{service_queue}::live-inspector", + 240, # Data will be removed from Redis after 240 sec. + 'USR1' # Immediately kill child but don't exit + ) + end + + def service_queue + "abfworker::#{abf_worker_base_queue.gsub(/\_/, '-')}-#{id}" + end + end \ No newline at end of file diff --git a/spec/factories/project_statistic.rb b/spec/factories/project_statistic.rb new file mode 100644 index 000000000..ec381ef09 --- /dev/null +++ b/spec/factories/project_statistic.rb @@ -0,0 +1,7 @@ +# -*- encoding : utf-8 -*- +FactoryGirl.define do + factory :project_statistic do + association :project, :factory => :project + association :arch, :factory => :arch + end +end diff --git a/spec/models/project_statistic_spec.rb b/spec/models/project_statistic_spec.rb new file mode 100644 index 000000000..e13a0ec20 --- /dev/null +++ b/spec/models/project_statistic_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe ProjectStatistic do + + context 'ensures that validations and associations exist' do + it { should belong_to(:project) } + it { should belong_to(:arch) } + + it { should validate_presence_of(:project_id) } + it { should validate_presence_of(:arch_id) } + it { should validate_presence_of(:average_build_time) } + it { should validate_presence_of(:build_count) } + + it { should_not allow_mass_assignment_of(:project_id) } + it { should_not allow_mass_assignment_of(:arch_id) } + + it 'uniqueness of project_id and arch_id' do + FactoryGirl.create(:project_statistic) + should validate_uniqueness_of(:project_id).scoped_to(:arch_id) + end + + end + +end