2014-09-03 70 views
3

我在後臺有一個更長的運行任務,以及我如何讓我的後臺任務的拉狀態或者更好地將任務完成傳達給我的前端?導軌中的拉/推狀態3

背景:

基本上我的應用程序使用第三方服務來處理數據,所以我想這個外部Web服務工作量不阻止所有傳入的請求到我的網站,所以我把這個電話背景中工作(我使用sidekiq)。所以當這項任務完成時,我正在考慮向某個控制器發送webhook,該控制器會通知前端該任務已完成。

我該怎麼做?有更好的解決方案嗎?

更新:

我的應用程序託管在Heroku

更新II:

我做了一些研究課題,我發現我可以創建一個單獨的在heroku上處理這個問題的應用程序,找到這個例子:

https://github.com/heroku-examples/ruby-websockets-chat-demo

這個長時間運行的任務將在每個用戶的網站上運行很多流量,這是一個好主意嗎?

+0

使用HTML 5 socket-io會非常棘手,所以我建議你使用Javascript(例如jQuery)並用定時器('interval'函數)輪詢 – 2014-09-03 21:41:44

+0

我認爲這是一個更具擴展性的解決方案,它也是Heroku兼容的方式是將後臺作業寫入用戶的數據庫行或快速備用(如redis實例),並讓客戶端在定時器上輪詢狀態。 – Gene 2014-09-06 17:47:50

+0

這個問題是關於如何實現它的後端部分(啓動作業,報告狀態回到應用程序)還是關於前端部分(如何通知瀏覽器狀態更改)? – spickermann 2014-09-07 01:37:43

回答

3

我會使用pub/sub系統來執行此操作,例如FayePusher。這背後的想法是,你會將長時間運行的作業的狀態發佈到一個頻道,然後會導致該頻道的所有用戶被通知狀態改變。

例如,你的工作亞軍之內,你可以用類似通知王菲狀態變化:

client = Faye::Client.new('http://localhost:9292/') 
client.publish('/jobstatus', {id: jobid, status: 'in_progress'}) 

,然後在前端就可以使用JavaScript訂閱該頻道:

var client = new Faye.Client('http://localhost:9292/'); 

client.subscribe('/jobstatus', function(message) { 
    alert('the status of job #' + message.jobid + ' changed to ' + message.status); 
}); 

以這種方式使用發佈/訂閱系統可以讓您將實時頁面事件與主應用程序分開進行擴展 - 您可以在另一臺服務器上運行Faye。您也可以使用Pusher等託管(和付費)解決方案,並讓他們負責擴展您的基礎架構。

另外值得一提的是,Faye使用,這意味着它將在可用的情況下使用websockets,並在不使用web-socket的情況下進行長輪詢。

+0

如何在faye上配置頻道名稱?默認似乎是'messages/new' – 2014-09-08 17:42:14

+0

@GandalfStormCrow - 頻道名稱是您訂閱或發佈的任何內容,您無需預先配置它。 – Mikey 2014-09-09 10:21:37

+1

http://railscasts.com/episodes/260-messaging-with-faye http://railscasts.com/episodes/316-private-pub – 2014-09-11 14:52:23

1

我們有這種模式,並使用兩種不同的方法。在這兩種情況下,後臺作業都使用Resque運行,但您可能會做類似DelayedJobSidekiq的操作。

輪詢

在輪詢方法,我們已經在頁面上的JavaScript對象,設置了超時輪詢從軌道HTML視圖傳遞給它的URL。

這會導致對提供的URL進行Ajax(「腳本」)調用,這意味着Rails會查找JS模板。所以我們用它來響應狀態並觸發一個事件來響應對象是否可用。

這有些複雜,我現在不推薦它。

套接字

更好的解決方案,我們發現使用WebSockets(帶墊片)。在我們的例子中,我們使用PubNub,但有很多服務來處理這個問題。這可以保持輪詢/開放連接關閉您的Web服務器,並且比運行處理這些連接所需的服務器更具成本效益。

您已經聲明您正在尋找前端解決方案,並且您可以使用PubNub的客戶端JavaScript庫處理所有前端。

下面是我們如何從後端通知PubNub的粗略想法。

class BackgroundJob 
    @queue = :some_queue 

    def perform 
    // Do some action 
    end 

    def after_perform 
    publish some_state, client_channel 
    end 

    private 

    def publish some_state, client_channel 
    Pubnub.new(
     publish_key: Settings.pubnub.publish_key, 
     subscribe_key: Settings.pubnub.subscribe_key, 
     secret_key: Settings.pubnub.secret_key 
    ).publish(
     channel: client_channel, 
     message: some_state.to_json, 
     http_sync: true 
    ) 
    end 
end 
1

,我能想到的最簡單的方法是,你在你的數據庫設置一個標誌,當任務完成後,和您的前端(圖)週期性地發送一個Ajax請求,以檢查在數據庫船旗國。如果標誌已設置,則在視圖中採取適當的操作。以下是代碼示例:

由於您建議此長時間運行的任務需要爲每個用戶運行,因此讓我們添加一個布爾值爲users表 - task_complete。當您添加作業sidekiq,你可以取消設置標誌:

# Sidekiq worker: app/workers/task.rb 

class Task 
include Sidekiq::Worker 
    def perform(user_id) 
    user = User.find(user_id) 
    # Long running task code here, which executes per user 
    user.task_complete = true 
    user.save! 
    end 
end 

# When adding the task to sidekiq queue 
user = User.find(params[:id]) 
# flag would have been set to true by previous execution 
# In case it is false, it means sidekiq already has a job entry. We don't need to add it again 
if user.task_complete? 
    Task.perform_async(user.id) 
    user.task_complete = false 
    user.save! 
end 

在視圖中,您可以定期檢查標誌是否被設置使用Ajax請求:

<script type="text/javascript"> 
var complete = false; 
(function worker() { 
    $.ajax({ 
    url: 'task/status/<%= @user.id %>', 
    success: function(data) { 
     // update the view based on ajax request response in case you need to 
    }, 
    complete: function() { 
     // Schedule the next request when the current one's complete, and in case the global variable 'complete' is set to true, we don't need to fire this ajax request again - task is complete. 
     if(!complete) { 
     setTimeout(worker, 5000); //in miliseconds 
     } 
    } 
    }); 
})(); 
</script> 

# status action which returns the status of task 
# GET /task/status/:id 
def status 
    @user = User.find(params[:id]) 
end 

# status.js.erb - add view logic based on what you want to achieve, given whether the task is complete or not 

<% if @user.task_complete? %> 
    $('#success').show(); 
    complete = true; 
<% else %> 
    $('#processing').show(); 
<% end %> 

您可以根據超時在你的任務的平均執行時間是什麼。假設你的任務平均需要10分鐘,所以他們沒有必要以5秒的頻率檢查它。

此外,在情況下,你的任務執行頻率是複雜的東西(而不是每天1次),你可能需要添加時間戳task_completed_at,並立足於標誌和時間戳的組合你的邏輯。

至於這部分: 「這個長時間運行的任務將在每個用戶上運行,在一個流量很大的網站上,這是個好主意嗎?」

雖然像在獨立硬件上執行作業(sidekiq workers)這樣的體系結構更改將有所幫助,但我沒有看到此方法的問題。這些都是輕量級的ajax調用,並且一些內置到您的javascript中的智能(如全局完成標誌)可以避免不必要的請求。如果你有很大的流量,並且DB讀/寫是一個問題,那麼你可能想將該標誌直接存儲到redis而不是(因爲你已經有了它的sidekiq)。我相信這將解決您的讀/寫問題,並且我不認爲它會導致問題。這是我能想到的最簡單最清晰的方法,儘管您可以嘗試通過大多數現代瀏覽器支持的websockets實現相同的功能(儘管可能會導致舊版本出現問題)。

+0

這就是我在回答「投票」時提到的問題。是的,它的工作原理。當你嘗試去適應新的功能時,你最終要編寫很多代碼來保持連接並保持很多東西。不過,它運作良好,我們仍然使用它。 – jwadsack 2014-09-11 20:10:03