我应该如何使用Scotty(Haskell)处理长时间运行的HTTP请求



我正在制作一个简单的web应用程序,它可以在文本中查找颜色单词,并绘制有关它们的统计信息。如果不是太忙的话,你可以在colors.jonreve.com上测试一下。我正在使用Scotty web框架来处理web内容。它适用于短文本,但长文本,如长篇小说,需要很长时间,浏览器通常会超时。所以我猜我需要的是通过Jquery AJAX或其他方式发送表单,然后让服务器每隔一段时间发送JSON及其状态("正在加载文件"、"正在计数颜色"等(,然后当它收到"成功"信号时,重定向到其他URL?

这是我第一次尝试做这样的事情,所以如果这一切听起来都不知情,请原谅我。我也注意到有一些类似的问题,但我有一种感觉,斯科蒂处理事情的方式与大多数设置有点不同。我注意到有一些函数用于设置原始输出、设置标题等等。我是否尝试在分析的每个阶段发出特定的信号?考虑到哈斯克尔对副作用的处理,我该怎么做?在这里,我甚至很难想出最好的方法。

我可能会设置一个接受POST请求的端点,而不是一个长时间运行的GET请求。POST将立即返回,并在响应体中包含两个链接:

  • 一个指向表示任务结果的新资源的链接,该资源不会立即可用。在此之前,对结果的GET请求可能返回409(冲突(。

  • 一个到相关的、立即可用的资源的链接,表示在执行任务时发出的通知。

一旦客户端成功获取任务结果资源,它就可以将其删除。这将删除任务结果资源和相关的通知资源。

对于每个POST请求,您都需要生成一个后台工作线程。您还需要一个后台线程来删除变旧的任务结果(因为客户端可能是惰性的,并且不会调用DELETE(。这些线程将与MVars、TVars、信道或类似方法进行通信。

现在的问题是:如何最好地处理服务器发出的通知?有几个选项:

  • 只需定期轮询客户端的通知资源。缺点:潜在的许多HTTP请求,通知没有及时收到
  • 长轮询。GET请求的序列保持打开直到服务器想要发出一些通知,或者直到超时
  • 服务器发送了事件。wai extra对此有支持,但我不知道如何将一个原始的waiApplication挂回Scotty
  • websockets。不知道如何与斯科蒂融合

这是一个长轮询机制的服务器端框架。一些初步进口:

{-# LANGUAGE NumDecimals #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_) -- from async
import Control.Concurrent.STM -- from stm
import Control.Concurrent.STM.TMChan -- from stm-chans
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (ToJSON) -- from aeson
import Data.Foldable (for_)
import Data.Text (Text) 
import Web.Scotty

这是主要代码。

main :: IO ()
main =
do
chan <- atomically $ newTMChan @Text
concurrently_
( do
for_
["starting", "working on it", "finishing"]
( msg -> do
threadDelay 10e6
atomically $ writeTMChan chan msg
)
atomically $ closeTMChan chan
)
( scotty 3000
$ get "/notifications"
$ do
mmsg <- liftIO $ atomically $ readTMChan chan
json $
case mmsg of
Nothing -> ["closed!"]
Just msg -> [msg]
)

有两个并发线程。一个以10秒的间隔将消息馈送到可关闭的通道中,另一个运行Scotty服务器,在该服务器中,每次GET调用都会挂起,直到新消息到达通道。

使用curl从bash测试它,我们应该看到一系列消息:

bash$ for run in {1..4}; do curl -s localhost:3000/notifications ; done
["starting"]["working on it"]["finishing"]["closed!"]

为了进行比较,这里是基于服务器发送事件的解决方案的框架。不过,它使用yesod而不是scotty,因为yesod提供了一种将管理事件的wai额外Application挂起作为处理程序的方法。

Haskell代码

{-# LANGUAGE NumDecimals #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_) -- from async
import Control.Concurrent.STM -- from stm
import Control.Concurrent.STM.TMChan -- from stm-chans
import Control.Monad.IO.Class (liftIO)
import Data.Binary.Builder -- from binary
import Data.Foldable (for_)
import Network.Wai.EventSource -- from wai-extra
import Network.Wai.Middleware.AddHeaders -- from wai-extra
import Yesod -- from yesod
data HelloWorld = HelloWorld (TMChan ServerEvent)
mkYesod
"HelloWorld"
[parseRoutes|
/foo FooR GET
|]
instance Yesod HelloWorld
getFooR :: Handler ()
getFooR = do
HelloWorld chan <- getYesod
sendWaiApplication
. addHeaders [("Access-Control-Allow-Origin", "*")]
. eventStreamAppRaw
$ send flush ->
let go = do
mevent <- liftIO $ atomically $ readTMChan chan
case mevent of
Nothing -> do
send CloseEvent
flush
Just event -> do
send event
flush
go
in go
main :: IO ()
main =
do
chan <- atomically $ newTMChan
concurrently_
( do
for_
[ ServerEvent
(Just (fromByteString "ev"))
(Just (fromByteString "id1"))
[fromByteString "payload1"],
ServerEvent
(Just (fromByteString "ev"))
(Just (fromByteString "id2"))
[fromByteString "payload2"],
ServerEvent
(Just (fromByteString "ev"))
(Just (fromByteString "eof"))
[fromByteString "payload3"]
]
( msg -> do
threadDelay 10e6
atomically $ writeTMChan chan msg
)
atomically $ closeTMChan chan
)
( warp 3000 (HelloWorld chan)
)

还有一个小的空白页面,用来测试服务器发送的事件。消息显示在浏览器控制台上:

<!DOCTYPE html>
<html lang="en">
<body>
</body>
<script>
window.onload = function() {
var source = new EventSource('http://localhost:3000/foo'); 
source.onopen = function () { console.log('opened'); }; 
source.onerror = function (e) { console.error(e); }; 
source.addEventListener('ev', (e) => {
console.log(e);
if (e.lastEventId === 'eof') {
source.close();
}
});
}
</script>
</html>

相关内容

最新更新