Commit 3cb84e06 authored by James Lopez's avatar James Lopez Committed by Rémy Coutable

Remove user activities table and use redis instead of PG for recording activities

Refactored specs and added a post deployment migration to remove the activity users table.
parent 2951a854
...@@ -101,7 +101,6 @@ class User < ActiveRecord::Base ...@@ -101,7 +101,6 @@ class User < ActiveRecord::Base
has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
has_one :user_activity, dependent: :destroy
# Issues that a user owns are expected to be moved to the "ghost" user before # Issues that a user owns are expected to be moved to the "ghost" user before
# the user is destroyed. If the user owns any issues during deletion, this # the user is destroyed. If the user owns any issues during deletion, this
...@@ -159,7 +158,6 @@ class User < ActiveRecord::Base ...@@ -159,7 +158,6 @@ class User < ActiveRecord::Base
alias_attribute :private_token, :authentication_token alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :path, to: :namespace, allow_nil: true, prefix: true
delegate :last_activity_at, to: :user_activity, allow_nil: true
state_machine :state, initial: :active do state_machine :state, initial: :active do
event :block do event :block do
...@@ -951,6 +949,14 @@ class User < ActiveRecord::Base ...@@ -951,6 +949,14 @@ class User < ActiveRecord::Base
end end
end end
def record_activity
Gitlab::Redis.with do |redis|
redis.zadd('user/activities', Time.now.to_i, self.username)
end
end
private
def access_level=(new_level) def access_level=(new_level)
new_level = new_level.to_s new_level = new_level.to_s
return unless %w(admin regular).include?(new_level) return unless %w(admin regular).include?(new_level)
......
class UserActivity < ActiveRecord::Base
belongs_to :user, inverse_of: :user_activity
validates :user, uniqueness: true, presence: true
validates :last_activity_at, presence: true
# Updated version of http://apidock.com/rails/ActiveRecord/Timestamp/touch
# That accepts a new record.
def touch
current_time = current_time_from_proper_timezone
if persisted?
update_column(:last_activity_at, current_time)
else
self.last_activity_at = current_time
save!(validate: false)
end
end
end
...@@ -14,13 +14,9 @@ module Users ...@@ -14,13 +14,9 @@ module Users
private private
def record_activity def record_activity
user_activity.touch @author.record_activity
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}") Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}")
end end
def user_activity
UserActivity.find_or_initialize_by(user: @author)
end
end end
end end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class DropUserActivitiesTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
drop_table :user_activities
end
end
...@@ -1230,13 +1230,6 @@ ActiveRecord::Schema.define(version: 20170408033905) do ...@@ -1230,13 +1230,6 @@ ActiveRecord::Schema.define(version: 20170408033905) do
add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree
add_index "uploads", ["path"], name: "index_uploads_on_path", using: :btree add_index "uploads", ["path"], name: "index_uploads_on_path", using: :btree
create_table "user_activities", force: :cascade do |t|
t.integer "user_id"
t.datetime "last_activity_at", null: false
end
add_index "user_activities", ["user_id"], name: "index_user_activities_on_user_id", unique: true, using: :btree
create_table "user_agent_details", force: :cascade do |t| create_table "user_agent_details", force: :cascade do |t|
t.string "user_agent", null: false t.string "user_agent", null: false
t.string "ip_address", null: false t.string "ip_address", null: false
...@@ -1396,5 +1389,4 @@ ActiveRecord::Schema.define(version: 20170408033905) do ...@@ -1396,5 +1389,4 @@ ActiveRecord::Schema.define(version: 20170408033905) do
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users" add_foreign_key "u2f_registrations", "users"
add_foreign_key "user_activities", "users", on_delete: :cascade
end end
...@@ -60,6 +60,13 @@ module API ...@@ -60,6 +60,13 @@ module API
rescue JSON::ParserError rescue JSON::ParserError
{} {}
end end
def log_user_activity(actor)
commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS +
Gitlab::GitAccess::GIT_ANNEX_COMMANDS
::Users::ActivityService.new(actor, 'Git SSH').execute if commands.include?(params[:action])
end
end end
end end
end end
...@@ -16,7 +16,9 @@ describe SessionsController do ...@@ -16,7 +16,9 @@ describe SessionsController do
end end
end end
context 'when using valid password' do context 'when using valid password', :redis do
include UserActivitiesHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
it 'authenticates user correctly' do it 'authenticates user correctly' do
...@@ -41,7 +43,7 @@ describe SessionsController do ...@@ -41,7 +43,7 @@ describe SessionsController do
it 'updates the user activity' do it 'updates the user activity' do
expect do expect do
post(:create, user: { login: user.username, password: user.password }) post(:create, user: { login: user.username, password: user.password })
end.to change { user.reload.last_activity_at }.from(nil) end.to change { user_score }.from(0)
end end
end end
end end
......
FactoryGirl.define do
factory :user_activity do
last_activity_at { Time.now }
user
end
end
...@@ -147,7 +147,9 @@ describe API::Internal, api: true do ...@@ -147,7 +147,9 @@ describe API::Internal, api: true do
end end
end end
describe "POST /internal/allowed" do describe "POST /internal/allowed", :redis do
include UserActivitiesHelpers
context "access granted" do context "access granted" do
before do before do
project.team << [user, :developer] project.team << [user, :developer]
...@@ -181,7 +183,7 @@ describe API::Internal, api: true do ...@@ -181,7 +183,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
expect(key.user.reload.last_activity_at.to_i).to eq(Time.now.to_i) expect(user_score).to be_zero
end end
end end
...@@ -192,7 +194,7 @@ describe API::Internal, api: true do ...@@ -192,7 +194,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
expect(key.user.reload.last_activity_at.to_i).to eq(Time.now.to_i) expect(user_score).not_to be_zero
end end
end end
...@@ -203,7 +205,7 @@ describe API::Internal, api: true do ...@@ -203,7 +205,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(key.user.reload.last_activity_at.to_i).to eq(Time.now.to_i) expect(user_score).not_to be_zero
end end
end end
...@@ -214,7 +216,7 @@ describe API::Internal, api: true do ...@@ -214,7 +216,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(key.user.reload.last_activity_at.to_i).to eq(Time.now.to_i) expect(user_score).to be_zero
end end
context 'project as /namespace/project' do context 'project as /namespace/project' do
...@@ -250,7 +252,7 @@ describe API::Internal, api: true do ...@@ -250,7 +252,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey expect(json_response["status"]).to be_falsey
expect(key.user.reload.last_activity_at).to be_nil expect(user_score).to be_zero
end end
end end
...@@ -260,7 +262,7 @@ describe API::Internal, api: true do ...@@ -260,7 +262,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey expect(json_response["status"]).to be_falsey
expect(key.user.reload.last_activity_at).to be_nil expect(user_score).to be_zero
end end
end end
end end
...@@ -278,7 +280,7 @@ describe API::Internal, api: true do ...@@ -278,7 +280,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey expect(json_response["status"]).to be_falsey
expect(key.user.reload.last_activity_at).to be_nil expect(user_score).to be_zero
end end
end end
...@@ -288,7 +290,7 @@ describe API::Internal, api: true do ...@@ -288,7 +290,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey expect(json_response["status"]).to be_falsey
expect(key.user.reload.last_activity_at).to be_nil expect(user_score).to be_zero
end end
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe EventCreateService, services: true do describe EventCreateService, services: true do
include UserActivitiesHelpers
let(:service) { EventCreateService.new } let(:service) { EventCreateService.new }
describe 'Issues' do describe 'Issues' do
...@@ -111,6 +113,19 @@ describe EventCreateService, services: true do ...@@ -111,6 +113,19 @@ describe EventCreateService, services: true do
end end
end end
describe '#push', :redis do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
it 'creates a new event' do
expect { service.push(project, user, {}) }.to change { Event.count }
end
it 'updates user last activity' do
expect { service.push(project, user, {}) }.to change { user_score }
end
end
describe 'Project' do describe 'Project' do
let(:user) { create :user } let(:user) { create :user }
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
...@@ -129,17 +144,4 @@ describe EventCreateService, services: true do ...@@ -129,17 +144,4 @@ describe EventCreateService, services: true do
it { expect { subject }.to change { Event.count }.from(0).to(1) } it { expect { subject }.to change { Event.count }.from(0).to(1) }
end end
end end
describe '#push' do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
it 'creates a new event' do
expect { service.push(project, user, {}) }.to change { Event.count }
end
it 'updates user last activity' do
expect { service.push(project, user, {}) }.to change { user.last_activity_at }
end
end
end end
require 'spec_helper' require 'spec_helper'
describe Users::ActivityService, services: true do describe Users::ActivityService, services: true do
include UserActivitiesHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
subject(:service) { described_class.new(user, 'type') } subject(:service) { described_class.new(user, 'type') }
describe '#execute' do describe '#execute', :redis do
context 'when last activity is nil' do context 'when last activity is nil' do
it 'sets the last activity timestamp' do before do
service.execute service.execute
end
it 'sets the last activity timestamp for the user' do
expect(last_hour_members).to eq([user.username])
end
it 'updates the same user' do
service.execute
expect(last_hour_members).to eq([user.username])
end
expect(user.last_activity_at).not_to be_nil it 'updates the timestamp of an existing user' do
Timecop.freeze(Date.tomorrow) do
expect { service.execute }.to change { user_score }.to(Time.now.to_i)
end end
end end
context 'when activity_at is not nil' do describe 'other user' do
it 'updates the activity multiple times' do it 'updates other user' do
activity = create(:user_activity, user: user) other_user = create(:user)
described_class.new(other_user, 'type').execute
Timecop.travel(activity.last_activity_at + 1.minute) do expect(last_hour_members).to match_array([user.username, other_user.username])
expect { service.execute }.to change { user.reload.last_activity_at }
end end
end end
end end
......
module UserActivitiesHelpers
def last_hour_members
Gitlab::Redis.with do |redis|
redis.zrangebyscore(user_activities_key, 1.hour.ago.to_i, Time.now.to_i)
end
end
def user_score
Gitlab::Redis.with do |redis|
redis.zscore(user_activities_key, user.username).to_i
end
end
def user_activities_key
'user/activities'
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment