Skip to content

Commit

Permalink
feat: counters
Browse files Browse the repository at this point in the history
  • Loading branch information
yamax2 committed Jul 12, 2019
1 parent e0d8686 commit 7953475
Show file tree
Hide file tree
Showing 21 changed files with 211 additions and 6 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ dip rails credentials:edit
```

### TODO:
* set main photo for rubric action
* views counter
* rubrics order

* set main photo for rubric action
* photo edit action
* map in photo listing

* videos
* tracks
* clear tmp job
Expand Down
4 changes: 4 additions & 0 deletions app/decorators/photo_decorator.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
class PhotoDecorator < Draper::Decorator
delegate_all

def current_views
views + inc_counter
end

def image_size(size = :thumb)
thumb_width = thumb_width(size)

Expand Down
11 changes: 11 additions & 0 deletions app/jobs/photos/dump_views_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Photos
class DumpViewsJob
include Sidekiq::Worker

def perform
RedisMutex.with_lock('counters:photo', block: 5.minutes, expire: 30.minutes) do
::Counters::DumpService.call!(model_klass: ::Photo)
end
end
end
end
10 changes: 10 additions & 0 deletions app/models/concerns/countable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Countable
def inc_counter
return unless persisted?

RedisClassy.
redis.
incr("counters:#{self.class.to_s.underscore}:#{id}").
to_i
end
end
2 changes: 1 addition & 1 deletion app/models/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def rubrics
@rubrics = rubrics.
with_photos.
preload(:main_photo).
order(:id).
order(id: :desc).
decorate
end

Expand Down
2 changes: 2 additions & 0 deletions app/models/photo.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# photo model, no state machine!
class Photo < ApplicationRecord
include Countable

JPEG_IMAGE = 'image/jpeg'.freeze
PNG_IMAGE = 'image/png'.freeze

Expand Down
36 changes: 36 additions & 0 deletions app/services/counters/dump_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Counters
class DumpService
include ::Interactor

delegate :model_klass, to: :context

def call
# batches?
redis.
scan_each(match: key_for('*')).
each do |key|
model = model_klass.where(id: key.gsub(/[^\d]+/, '').to_i).first
dump_counter(model) if model
end
end

private

delegate :redis, to: RedisClassy

def dump_counter(model)
key = key_for(model.id)

value = redis.multi do
redis.get(key)
redis.del(key)
end.first

model.update_column(:views, model.views + value.to_i) if value.present?
end

def key_for(id)
"counters:#{model_klass.to_s.underscore}:#{id}"
end
end
end
6 changes: 6 additions & 0 deletions app/views/photos/show.slim
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,11 @@
td
= link_to t('.download'), @photos.current.url, target: '_blank', title: @photos.current.original_filename

tr
th
= t('.views')
td
= @photos.current.current_views

p.photo-description
= @photos.current.description
1 change: 1 addition & 0 deletions config/locales/ru.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ ru:
map: На карте
show_map: Показать
size: Размер
views: Просмотры
rubrics:
name:
rubrics_count_text: ', подрубрик: %{rubrics_count}'
Expand Down
4 changes: 4 additions & 0 deletions config/schedule.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
yandex_refresh:
cron: '0 1 * * *'
class: Yandex::RefreshTokensJob

dump_photo_views:
cron: '0 2 * * *'
class: Photos::DumpViewsJob
5 changes: 5 additions & 0 deletions db/migrate/20190712053954_add_views_to_photos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddViewsToPhotos < ActiveRecord::Migration[5.2]
def change
add_column :photos, :views, :bigint, default: 0, null: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2019_06_27_051315) do
ActiveRecord::Schema.define(version: 2019_07_12_053954) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand All @@ -34,6 +34,7 @@
t.datetime "updated_at", null: false
t.string "md5", limit: 32, null: false
t.string "sha256", limit: 64, null: false
t.bigint "views", default: 0, null: false
t.index ["md5", "sha256"], name: "uq_photos", unique: true
t.index ["rubric_id"], name: "index_photos_on_rubric_id"
t.index ["yandex_token_id"], name: "index_photos_on_yandex_token_id", where: "(yandex_token_id IS NOT NULL)"
Expand Down
4 changes: 4 additions & 0 deletions docker/docker-compose.development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ services:
- default
- frontend
dns: $DIP_DNS
depends_on:
- db
- redis
environment:
- EDITOR=nano

Expand All @@ -28,6 +31,7 @@ services:
dns: $DIP_DNS
depends_on:
- db
- redis
environment:
- VIRTUAL_HOST=*.photostorage.$DOCKER_TLD
- VIRTUAL_PATH=/
Expand Down
2 changes: 1 addition & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ services:
volumes:
- .:/app
- ../../go:/go
command: 'go run proxy_service/main.go -host=db -user=postgres -db=photos'
command: 'go run proxy_service/main.go -host=db -user=postgres -db=photos -listen=0.0.0.0'
expose:
- '9000'
4 changes: 3 additions & 1 deletion proxy_service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ func main() {
db_host := flag.String("host", "localhost", "db host")
db_user := flag.String("user", "postgres", "db user")
db_name := flag.String("db", "photos", "database")
listen_addr := flag.String("listen", "127.0.0.1", "listen_addr")

flag.Parse()

connection_str := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable", *db_host, *db_user, *db_name)
Expand Down Expand Up @@ -51,7 +53,7 @@ func main() {

proxy := httputil.NewSingleHostReverseProxy(remote)
http.Handle("/", &ProxyHandler{env, proxy})
err = http.ListenAndServe("127.0.0.1:9000", nil)
err = http.ListenAndServe(fmt.Sprintf("%s:9000", *listen_addr), nil)
if err != nil {
panic(err)
}
Expand Down
17 changes: 17 additions & 0 deletions spec/decorators/photo_decorator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@
)
end

describe '#current_views' do
before { RedisClassy.flushdb }
after { RedisClassy.flushdb }

let(:photo) { create :photo, :fake, views: 1_000, local_filename: 'test' }

context 'when first call' do
it { expect(subject.current_views).to eq(1_001) }
end

context 'when second call' do
before { subject.inc_counter }

it { expect(subject.current_views).to eq(1_002) }
end
end

describe '#image_size' do
let(:photo) { create :photo, :fake, width: 1_000, height: 2_000, local_filename: 'test' }

Expand Down
12 changes: 12 additions & 0 deletions spec/jobs/photos/dump_views_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require 'rails_helper'

RSpec.describe Photos::DumpViewsJob do
before do
allow(Counters::DumpService).to receive(:call!)
described_class.perform_async
end

it do
expect(Counters::DumpService).to have_received(:call!).with(model_klass: Photo)
end
end
3 changes: 3 additions & 0 deletions spec/models/photo_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
it { is_expected.to have_db_column(:content_type).of_type(:string).with_options(null: false, limit: 30) }
it { is_expected.to have_db_column(:width).of_type(:integer).with_options(null: false, default: 0) }
it { is_expected.to have_db_column(:height).of_type(:integer).with_options(null: false, default: 0) }
it { is_expected.to have_db_column(:views).of_type(:integer).with_options(null: false, default: 0) }

it { is_expected.to have_db_column(:created_at).of_type(:datetime).with_options(null: false) }
it { is_expected.to have_db_column(:updated_at).of_type(:datetime).with_options(null: false) }
Expand Down Expand Up @@ -279,4 +280,6 @@
and change { photo.size }.from(0).to(File.size(tmp_file))
end
end

it_behaves_like 'model with counter', :photo
end
42 changes: 42 additions & 0 deletions spec/services/counters/dump_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require 'rails_helper'

RSpec.describe Counters::DumpService do
before { RedisClassy.flushdb }
after { RedisClassy.flushdb }

let(:redis) { RedisClassy.redis }

subject { described_class.call!(model_klass: Photo) }

let!(:photo_without_views) { create :photo, :fake, local_filename: 'test', views: 0 }
let!(:photo_with_views) { create :photo, :fake, local_filename: 'test', views: 1_000 }
let!(:wrong_photo) { create :photo, :fake, local_filename: 'test', views: 2_000 }

context 'when no counters in redis' do
it do
expect { subject }.
to change { photo_without_views.reload.views }.by(0).
and change { photo_with_views.reload.views }.by(0).
and change { wrong_photo.reload.views }.by(0)
end
end

context 'when information presents' do
before do
redis.set("counters:photo:#{photo_without_views.id}", 100)
redis.set("counters:photo:#{photo_with_views.id}", 10)
redis.set("counters:photo:#{photo_with_views.id * 2}", 1_000)
end

context 'when regular call' do
it do
expect { subject }.
to change { photo_without_views.reload.views }.by(100).
and change { photo_with_views.reload.views }.by(10).
and change { wrong_photo.reload.views }.by(0)

expect(redis.keys).to match_array(["counters:photo:#{photo_with_views.id * 2}"])
end
end
end
end
Empty file removed spec/support/.keep
Empty file.
44 changes: 44 additions & 0 deletions spec/support/model_with_counter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
RSpec.shared_context 'model with counter' do |factory|
describe '#inc_counter' do
before { RedisClassy.flushdb }
after { RedisClassy.flushdb }

let(:redis) { RedisClassy.redis }

subject { model.inc_counter }

context 'when persisted' do
let(:model) { create :photo, :fake, local_filename: 'test' }

context 'and first view' do
it do
expect { subject }.
to change { redis.get("counters:#{model.class.to_s.underscore}:#{model.id}") }.from(nil).to('1')

is_expected.to eq(1)
end
end

context 'and second view' do
before { model.inc_counter }

it do
expect { subject }.
to change { redis.get("counters:#{model.class.to_s.underscore}:#{model.id}") }.from('1').to('2')

is_expected.to eq(2)
end
end
end

context 'when not persisted' do
let(:model) { build factory }

it do
expect { subject }.not_to(change { RedisClassy.redis.keys('counters:*') })

is_expected.to be_nil
end
end
end
end

0 comments on commit 7953475

Please sign in to comment.