12/08/2018, 13:12

Auto thay đổi STATE bằng StateMachine và sidekiq rails 4

I. Các khái niệm 1. Sidekiq Sidekiq là một gem hỗ trỡ xử lý ngầm dưới background mạnh mẽ cho Ruby. Nó nhằm mục đích là đơn giản để tích hợp với bất kỳ ứng dụng Rails hiện đại và hiệu suất cao hơn nhiều so với các giải pháp hiện có khác. Link github Link hướng dẫn cài đặt và sử dụng ...

I. Các khái niệm

1. Sidekiq

Sidekiq là một gem hỗ trỡ xử lý ngầm dưới background mạnh mẽ cho Ruby. Nó nhằm mục đích là đơn giản để tích hợp với bất kỳ ứng dụng Rails hiện đại và hiệu suất cao hơn nhiều so với các giải pháp hiện có khác.

  • Link github

  • Link hướng dẫn cài đặt và sử dụng

2. State machine

StateMachine là 1 gem được tạo ra để đơn giản hóa quản lý các hành vi của một Object. Bình thường, trạng thái của một Object lưu giữ bằng cách tạo ra nhiều thuộc tính và các hành động xử lý dựa trên các giá trị. Điều này có thể trở nên cồng kềnh và khó khăn để duy trì khi sự phức tạp của các Object bắt đầu tăng. StateMachine sẽ giúp bạn xử lý các hành động phức tạp trên, bằng cơ chế rất đơn giản.

  • Link github

II. Kết hợp giữa sidekiq và state_machine

Khi chúng ta muốn 1 hành động nào đó xảy ra một cách tự động hóa.

VD cụ thể: Ta có 1 User, ta muốn tạo ra hành động công việc hoạt động sau 10s khi User bấm nút create nó, sau 1 khoảng thời gian sau (thời gian được quy định bơi User) thì hoạt động đó trở về trạng thái stop và cộng điểm cho User.

1. Cài đặt state_machine

Nếu chúng ta không sử dụng gem thì phải viết các hàm:

class Test < ActiveRecord::Base
  belongs_to :user

  def start?
    state == "ongoing"
  end

  def pending

  end

  def start
    update! state: :ongoing
  end

  def after_start
    # Đây là chỗ xử lý tự động để khi bấm nút start sau khoảng thời gian state sẽ tự động về trạng thái stop
  end

  def stop

  end
end
```

Khi sử dụng gem chúng ta sẽ được code gọn hợn, đẹp hơn và dễ hiểu hơn

```ruby
class Test < ActiveRecord::Base
  belongs_to :user

  state_machine :state, initial: :creating do
    event :prepare do
      transition creating: :pending
    end

    event :start do
      transition pending: :ongoing
    end

    event :stop do
      transition all => :ended
    end

    after_transition on: :pending do |test|

    end
end
```

Code ở trên có nghĩa là khi chúng ta tạo ra 1 test thì `state` mặc định của nó sẽ là `creating`
và các event bên dưới là khi chúng ta có 1 record `@test` ta chỉ cần `@test.start` thì `state` của nó sẽ tự động chuyển về `ongoing`

#### 2. Cài state_machine với sidekiq

Ở trên ta vừa lấy ví dụ với `User` có nhiều bài `tests`. Ta tạo 1 project như sau

- Giao diện:

![home2.png](/uploads/8a6bec80-3e98-4028-a3e6-37239d4ab8e9.png)

Ta1 `user` đang có 10 point, và list các `tests` chưa có cái nào, giờ ta sẽ tạo nó, đồng thời chuyển nó về trạng thái `pending`, ở form nhập vào chúng ta sẽ có các thuộc tính `start_after`, `during`, `name`. trước khi tạo thì ta bật `terminal` cd đến project gõ `sidekiq` để bật server của sidekiq lên ;).

![modal.png](/uploads/ea501116-9e4e-4e8a-9c68-3d544e58e87f.png)

- `start_after` là thời gian sau khi bấm nút `Add` bài `test` đó sẽ được bắt đầu, thời điểm bắt đầu thì `test.state = ongoing`
- `during` là thời gian diễn ra bài `test` đó, lúc hết thời gian thì `test.state = ended`

Tất cả các trạng thái sẽ được chuyển tự động bằng phương thức `after_transition` của `state_machine`, trong phương thức này ta sẽ lập lịch cho nó tự động chuyển trạng thái bằng `sidekiq`.
- Ta sẽ tạo ra file `app/workers/test_worker.rb`:

```ruby
class TestWorker
  include Sidekiq::Worker

  def perform id, action
    TestWorker.delete_schedule id, action
    TestWorker.delete_schedule id, action
    test = Test.find_by id: id
    test.try action.to_sym
    test.user.increment! :point, 10 if action.to_s == "stop"
  end

  class << self
    def delete_schedule id, action
      jobs = Sidekiq::ScheduledSet.new.select do |retri|
        retri.klass == name && retri.item["args"] == [id, action]
      end
      jobs.each(&:delete)
    end
  end
end
```

Như vậy chúng ta đã khai báo 1 lịch cho `Test` và thời điểm đặt lịch như thế nào thì ta sẽ chuyển đến model `Test`, `app/models/test.rb`:

```ruby
class Test < ActiveRecord::Base
  attr_accessor :start_after, :during
  belongs_to :user

  state_machine :state, initial: :creating do

    # ...

    after_transition on: :prepare do |test|
      TestWorker.delete_schedule test.id, :start
      TestWorker.perform_at(test.starts_at, test.id, :start)
    end

    after_transition on: :start do |test|
      TestWorker.delete_schedule test.id, :stop
      TestWorker.perform_at(test.ends_at, test.id, :stop)
    end
  end
end
```

Như trên có nghĩa là sau khi `test` chuyển đến trạng thái `pending` thì sẽ đặt lịch cho `sidekiq` đã được định nghĩa ở trên, thực hiện hành động `start` cho `test` đó tại thời điểm `test.starts_at` được ghi bởi lúc tạo test.

-> Như vậy chúng ta đã cho `test.state` auto thay đổi từ lúc tạo ra `test` bằng cách sử dụng state_machine và sidekiq

- [Link video demo](https://youtu.be/p0bC57_OLas)

## III.             
0