回答最初的问题
TODO 1. SSE.new(response.stream, retry: 300), 返回 sse 数据时,会把 retry: 300 返回,这个是正常?
正常,https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation sse 返回的 field names 包括 event, data, id, retry,你代码中的 status:, message:不是标准的用法。
TODO 2. ensure 这里不可以 close, 因为 rescue_from 的执行是在 ensure 之后的,有没有更优雅的写法
ensure 应该放到def talk
里。正常的 talk 方法应该是
def talk
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 300, event: "event-name")
# sse的action一定是一个持续很长时间的action。
loop do
finished = Chats::GetAnswer.execute(params, sse)
break if finished
end
ensure
sse.close
end
你要是用纯血 rails 的话,Hotwire 那一套对 websocket 更友好,sse 查无此人了。
我其实之前做过一个 go 的 sse 转发服务。大体是
我编辑 0 楼回复不小心删掉了。原话是
原话是 Rails 不适合做 SSE,一个 SSE 要占用一个 puma thread。不如直接用 websocket,链接不占用 puma thread。 实在要用 SSE 可以用 go 实现 SSE,类似于https://anycable.io/ 的架构,SSE 链接是 go server 维护,Rails application 给 go server 发请求让 go server 执行 SSE 推送。
只有对 repo 有绝对话语权的人才能提大几千行的 PR。
brew --prefix openssl@3
rvm install 3.3.1 --with-openssl-dir=$(brew --prefix openssl)
我有一个练手项目是 rails + turbo + stimulus 写绝大部分页面,交互复杂页面用 svelte。
稍微看了一些回复,发现很多人都不知道 turbo 基本没有给 end user 的 api,甚至不知道 turbo 是干什么的就来评论
科普一下,对 turbo 库的使用者来说,turbo 是用 js 开发的还是 ts 开发的根本没区别,基本没有 js 方法给你调用。
叠甲:对于 turbo contributors 来说是有资格愤怒的。
比较好奇怎么做大家才觉得妥当?核心团队做出 break change 的决定后,然后花几个月时间在社区说服所有人然后再进行?
而且这是 turbo 7 升 8 的"break" change,任何库升大版本都会有很多不兼容旧版本的改动,都会有不同意见,很正常的事。
ENFP-A 每个字母都出现过了。
catch/throw
ActionCable 一般通过有 pubsub 功能的组件实现 broadcast,比如 redis https://redis.io/docs/manual/pubsub/。
每一个 rails web 进程都保存自己 websocket 连接以及这些连接都订阅了哪些 channel(actioncable 里的 channel)。 同时 web 进程也 subscribe 了 redis actioncable 的 channel(redis 的 channel,跟 actioncable 的 channel 不是一个东西)。
worker 里面调用 broadcast,实际是向 redis 的 actioncable channel 发一条信息,向“xx channel 广播 yy message”。 web 进程收到“xx channel 广播 yy message”,检查自己进程里有没有连接订阅过 xx channel,如果有的话就发送"yy message”。
ruby 3.2 a[5,1]
就是nil
了
https://rubyapi.org/2.7/o/string#method-i-5B-5D
Additionally, an empty string is returned when the starting index for a character range is at the end of the string.
https://guides.rubyonrails.org/active_support_instrumentation.html#cache-read-active-support 靠 APM 工具或者自己统计 hit/total
rails new app --minimal
先说一下结论免得你排查走弯路:抛ActiveRecord::ConnectionTimeoutError
错误的话 100% 是因为你的业务线程数跟 ActiveRecord
的本地数据库连接池大小不匹配(错误信息could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use
, https://github.com/rails/rails/blob/v7.0.4/activerecord/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb#L124), ActiveRecord::ConnectionTimeoutError
的注释:
Raised when a connection could not be obtained within the connection acquisition timeout period: because max connections in pool are in use.
可能的原因有
如果是数据库原因不能建立新的数据库连接,抛的错是 ActiveRecord::ConnectionNotEstablished,错误信息来自于具体数据库连接的库,例如 pg 的是: https://github.com/rails/rails/blob/v7.0.4/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L87
我好奇的是你的 ActiveRecord pool 设成 300 居然还能 checkout 不了新的数据库连接,我猜测你的应用应该有多种 rails 进程(比如 puma,worker,grpc 三种进程),报错的的那个进程 pool size 不一定是 300。
方便的话把堆栈也发一下(业务代码的堆栈可以过滤掉)
建议查一下 grpc 进程的数据库连接池大小。连接池大小一般都是通过环境变量来的,可能 grpc 进程的大小不是 300。 如果你是直连 postgres(不通过 pgbouncer 一类的中间件),连接池满的话 pg 不可能不到 70 个链接。
先看一下ActiveRecord::Base.connection_pool.size
。这个数是你 rails 进程的最大并发数。
rpc 也有一个线程池,我目前开了 200
相当于 200 个线程共享 ActiveRecord::Base.connection_pool.size 个数据库链接,如果 size 太小肯定不够用,从 pool 里 checkout 一个 connection 肯定会超时。
另外连 pg 建议通过 pgbouncer 连。
这个问题一般是应用线程抢不到数据库连接池里的链接,跟数据库负载关系不大。
我想了一种折中方案,就是通过实际调用 service 来准备数据,然后把这些数据 dump 一份,保存来用作 mock,这样 service 改变这个 dump 也不会变,可以很好地满足他想要隔离测试的想法。
因为你在 config 或者 initializer 中有数据库操作。assets:precompile 本身不需要连接数据库的,看一下错误堆栈,找到连数据库的代码,想办法避免启动的时候执行。
写测试。
require 'thread'
t1, t2, t3 = nil, nil, nil
def download
sleep rand(5..10)
end
t1 = Thread.new do
t = Time.now
download
t2.raise "STOP"
t3.raise "STOP"
puts "t1 download done."
rescue
puts "abort t1"
ensure
# teardown
puts "t1 duration: #{Time.now - t}"
end
t2 = Thread.new do
t = Time.now
download
t1.raise "STOP"
t3.raise "STOP"
puts "t2 download done."
rescue
puts "abort t2"
ensure
# teardown
puts "t2 duration: #{Time.now - t}"
end
t3 = Thread.new do
t = Time.now
download
t1.raise "STOP"
t2.raise "STOP"
puts "t3 download done."
rescue
puts "abort t3"
ensure
# teardown
puts "t3 duration: #{Time.now - t}"
end
[t1, t2, t3].map(&:join)
Ruby 可以通过Thread#raise
向另外一个线程抛异常来中断另一个线程。
turbo 启用的情况下,create action 要 render 一个 create.turbo_stream.erb, 在 create.turbo_stream.erb 用 turbo 相关标签声明你要替换/新增那些 html。
turbo 关闭(传统模式)是整页重新 render,开启则是服务端返回一些 html 片段,页面只局部更新。