工程师也会“相轻”的,跟不同背景的同事吹逼,聊到算法,设计模式以及“众所周知”的 pattern 一定不能露怯,否则将会被暗暗鄙视(其实大部分人都是半桶水,所以都会很默契的不会聊的很深入)
面试中算法有点像义务教育里那些感觉没用的课程,一是能快速筛掉学习能力差的人,二是算法确实对编码有影响。
三楼的需要在方法外定义 param
不需要,写里面也一样。
还不是很理解,为啥 broadcast 的时候要用 thread pool,因为 io 已经 异步了
ActionCable 的实现是在内存里维护一个订阅列表的 hash,key 是 channel,value 是一个订阅者回调 lambda 的 array,
回调 lambda : https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/channel/streams.rb#L146-L154
def worker_pool_stream_handler(broadcasting, user_handler, coder: nil)
handler = stream_handler(broadcasting, user_handler, coder: coder)
-> message do
connection.worker_pool.async_invoke handler, :call, message, connection: connection
end
end
broadcast 的时候:https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb#L36-L45
def broadcast(channel, message)
list = @sync.synchronize do
return if !@subscribers.key?(channel)
@subscribers[channel].dup
end
list.each do |subscriber|
invoke_callback(subscriber, message)
end
end
invoke_callback
最终是上面那段 lambda
-> message do
connection.worker_pool.async_invoke handler, :call, message, connection: connection
end
worker_pool
是一个 thread pool (Concurrent::ThreadPoolExecutor),async_invoke
是往 thread pool 里塞任务,任务异步执行,所以遍历 subscribers 时是不会阻塞的,而消息何时向全部 client 发完取决于 thread pool 消化任务是的速度了。而且整个 ActionCable 是公用一个 thread pool 的。
可以看一下 https://hashrocket.com/blog/posts/websocket-shootout 粗略比较了各种编程语言/框架 websocket 的指标。
结论
When it comes to websockets, Clojure, Elixir, and Go are all choices that balance great performance with ease of development. NodeJS is in a lower performance tier, but is still good enough for many uses. Rails can only be recommended when part of an existing Rails application with few concurrent clients. C++ offers the best performance, but it is hard to recommend due to the complexity and difficulty of development.
需要指出的是做测试的时候 Rails 6 刚出,puma 还没有用上 nio4r,所以 Rails 支持的连接数看起来弱爆了,但是不代表现在 Rails 的真实水平。
ActionCable 比较大的问题是 broadcast 是基于线程池的,线程池大小默认是 5(可以通过 config.action_cable.worker_pool_size 调整),换句话说一个 puma worker 同一时间只能同时向 5 个 websocket client 发消息
装饰器可以对原方法进行修饰,返回一个修改之后的方法,在打印日志,切面编程中运用广泛,你说 ruby 开放那你可以试一下在不改变函数名的情况下对原函数进行修改
module LogDecortor
def log_method *method_names
mod = Module.new do
method_names.each do |method_name|
define_method(method_name) do |*args, &blk|
puts "Method #{method_name}: start"
ret = super(*args, &blk)
puts "Method #{method_name}: end"
ret
end
end
end
self.prepend(mod)
end
end
class Foo
extend LogDecortor
log_method :bar
def bar
return 1
end
end
p Foo.new.bar
另外 Ruby 即使有不改变方法代码本身就改变方法行为的能力,但是实践中还是比较克制,像加日志、打点一类的需求有另外的方法,就不展开说了。
第二 javascript 只是我用来举例,可能你没懂我在说什么
只是没懂你的举的例子有什么太大的好处。
第三纯函数式语言是写不出复杂的系统的,或者说是不容易写出复杂的系统
纯函数式编程不容易写出复杂系统是因为程序员要求高一点。
所以这里的函数式编程就是一个概念,允许方法有副作用
"函数式编程就是一个概念"不是我的原话吗?我本意是现在市面上绝大部分面向对象语言都可以写“函数式编程”的代码,只是前端生态很推崇,别的领域还没有一要使用“函数式编程”的场景。
Ruby 2.7 曾经有过管道符 https://bugs.ruby-lang.org/issues/15799 ,你对函数式编程那么了解肯定知道管道符对函数式编程的意义,虽然种种原因取消了,但是可以反映出 Ruby 是拥抱函数式编程的,你可以期待一下
比如增加 python 的 AOP 特性 (装饰器)
Ruby 足够开放,不需要语言层面装饰器。
def 定义的方法名也是一个变量,可以覆盖(就是重复定义)类似 javascript,
javascript 还没有好到需要别的语言模仿吧 覆盖方法不就是 override 么……
然后支持函数式编程,高阶函数(支持传入函数,传出函数),现在来看还不能算真正支持函数式。
高阶函数核心在与“穿作用域”,而在“穿作用域”领域,Ruby 比大多数语言强很多。函数式编程也仅仅是个概念:要不你就用纯函数式编程语言,写不出违反函数式编程 pattern 的代码;要不你就用其他语言假装在函数式编程:有些领域大家都在假装,有些领域大家还没开始装,并不是说不能装,只是还没遇到需要装的场景。
学新语言还是要有空杯的心态。
Rails 原生的 websocket 比较弱,这种全靠 websocket 的框架还是观望一下比较好(实在忍不住可以用 phoenix live view)
看过Samuel Williams的 Ruby 处理百万 websocket 连接The Journey to One Million by Samuel Williams后一度对 Ruby 在稍微高一点的并发场景产生了幻想。
Samuel Williams 主推的是基于Fiber的Async做 ruby 的 websocket server,然而 Rails 团队似乎并没有打算用 Aysnc https://github.com/rails/rails/issues/35657
关于 websocket 另一条路是用 Anycable,用 golang 或者 erlang 做 websocket server,再通过一另一个 gRPC 服务跟 Rails 应用代码交互。Stimulusrelex 文档上说已经支持 Anycable 了,但是用 Anycable 增加了部署的复杂度,会不会违背了 Stimulusrelex 的初心
With StimulusReflex, you'll ship projects faster, with smaller teams and re-discover the joy of programming.
另外分享几个事情:
xxx.call(10)
可以写成xxx.(10)
或者xxx[10]
,都是语法糖,用多了讨人厌
param = []
define_method(:method1) do
define_method(:method2) do |x|
param.push(x)
end
return method(:method2)
end
f = method1
f.(10)
p param
微服务管理者能分山头,技术人员能折腾着玩,除了对产品不一定好,其他都照顾到了。
一般开发者不需要操心这个,对 rails 开发者来说,这部分细节是在 puma 等 webserver 这边。puma 之前版本是用的IO.select
操作 socket,绝大部分场景够用了。后来有了 actioncable 才用 nio4r(文中的 epoll),主要是突破IO.select
最多只能接 1024 个 socket 对象的限制 https://github.com/puma/puma/commit/e83a4954e4c0214d18beb594ddf598fafdf058d7#diff-8b7db4d5cc4b8f6dc8feb7030baa2478
没有必要把 B 放到 A 里面,你的问题是 A 以及子类依赖另外一个类,你需要把这个类传给 A 或者 A 的子类。把 B 放进 A 不解决问题。
require 'active_support/core_ext'
class B
def b
puts "A::B#b"
end
end
class A
class_attribute :b_like_class, default: B
def a
b_like_class.new.b
end
end
class B2 < B
def b
puts "B2.b"
super()
end
end
class A_CHILD < A
self.b_like_class = B2
end
A.new.a
A_CHILD.new.a
class A
class B
def b
puts "A::B#b"
end
end
def a
B.new.b
end
end
class A_CHILD < A
def a
b = B.new
def b.b
puts "A_CHILD#B.b"
super
end
b.b
end
end
报错的是 https://github.com/ruby-china/homeland/blob/master/app/controllers/admin/stats_controller.rb#L11
result[:count] = klass.unscoped.count # Line 11
...
def klass # Line 17
params[:model].camelize.safe_constantize
end
你的 url 里是model=note
,似乎代码里并没有这个 model。换一个别的试试看。
如何定义“优雅”?我面试的时候经常问别人
concern 用处有两个
解决依赖问题是通过 override Module#append_features
跟Module#included
两个钩子,希望多介绍这两个钩子。
concern 用途 一、用于把特定功能代码集合封装成一个 concern,提高代码可读性和方便代码重构; 二、把多个 model 或者 多个 controller 重复的功能集合代码封装成一个 concern。
用普通的 include/module 办不到吗。
<%= link_to 'CSR', 'www.baidu.com' %>
跟<%= link_to 'CSR', 'https://www.baidu.com' %>
的区别
免费获得接近 SPA 的页面切换体验不香吗,不会用就别用,多大点事。
全栈,前后端分离,微服务,中台是否合适都要看团队规模跟组织架构。Rails 适合小团队,提供非常实用的全栈的解决方案,turbolinks 用起来很开心;团队人多了大家编程水平跟对项目代码的整体理解参差不齐,前后端分离能解决一部分问题;分工越细致领导岗位也越多 (反着说也行)。
request_id 写到当前 request 对象中,差不多
middleware ActionDispatch::RequestId do
request.request_id = make_request_id
middleware Rails::Rack::Logger do
Rails.logger.tagged(request.reqeust_id) do
# Your Rails Application Code
end
end
end
能力还没强到可以填补学历的差距。
中了缓存还要 200ms 的话……优化空间很大
我写过查看 Rails 应用调用栈的工具,可能能帮你找一下瓶颈 (发现过我们应用一个请求调 8000 多次的方法) https://github.com/piecehealth/rails_watcher
可以把要统计的结果提前算出来存到另一个表里。这很像数据仓库里的 ETL 了
rexml 好像是纯 ruby 的
DistributedMutex
的实现是允许超时自动释放锁的,是一个不严谨的悲观锁。超时有 warning log,可以方便追踪,或者监控报警。第一优先级还是不要让操作超时。
真的要用悲观锁,可以参考 https://redis.io/topics/distlock
首先加锁后的操作如果占用太长时间会成为瓶颈,即第一优先级是尽量快的释放锁。
如果实在完不成,比如所有操作有 5 步,可以每步都 extend 一下锁,不用检查还有多少时间。
当 A 线程还在执行代码时(但是已经超过超时时间),B 线程发现锁已经超过超时时间,然后 B 设置了新的超时时间并获取了锁(仅仅在 A 线程执行完毕后在 log 中记录一条 warning),这种做法是否合理?感觉上即使一个线程的执行时间如果超过了超时时间,其他线程也不应该获取到这个锁,而是获取锁失败。
A 线程要负责释放锁,否则如果 A 异常退出那么就是死锁了。如果 A 在锁过期时间内执行不完,A 可以 extend 锁。
但是在 Discourse 中的使用方式都是下面这样,所以有点迷惑,这种方式使用的话是不是就不需要 Mutex.new.synchronize 来保证原子性了:
DistributedMutex.synchronize
借助 redis 实现了跨进程/机器的锁,Mutex#synchronize
实现了本进程起的线程级别的锁。的确可以只用 redis 实现线程级别的锁,但是毕竟有额外的 redis 读写操作,不如直接用内置的Mutex#synchronize
。
附:Rails 中,关于多线程方面的知识可以在哪里接触到?是一个或 n 个请求就会对应一个线程,又或者是一个 session 会对应一个线程?
Rails 用的多线程知识可能并不多,顶多是如何在多线程环境下初始化一个单例。多线程可以看 puma 或者 sidekiq。
看看你 rails 版本
verbose_query_logs
在https://github.com/rails/rails/blob/41bbd7cd0e4e950ea5a366f78b26b32ed6bd0644/activerecord/lib/active_record/core.rb#L26 定义的
class Form < ActiveRecord::Base
self.abstract_class = true
def self.inherited klass
klass.class_eval do
default_scope order("#{table_name}.form_date DESC")
end
end
end
class SubForm < Form
end