Class: PresenceChannel
- Inherits:
-
Object
- Object
- PresenceChannel
- Defined in:
- lib/presence_channel.rb
Overview
The server-side implementation of PresenceChannels. See also PresenceController and app/assets/javascripts/discourse/app/services/presence.js
Defined Under Namespace
Classes: Config, ConfigNotLoaded, InvalidAccess, InvalidConfig, NotFound, State
Constant Summary collapse
- DEFAULT_TIMEOUT =
60
- CONFIG_CACHE_SECONDS =
10
- GC_SECONDS =
24.hours.to_i
- MUTEX_TIMEOUT_SECONDS =
10
- MUTEX_LOCKED_ERROR =
"PresenceChannel mutex is locked"
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#message_bus_channel_name ⇒ Object
readonly
Returns the value of attribute message_bus_channel_name.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#timeout ⇒ Object
readonly
Returns the value of attribute timeout.
Class Method Summary collapse
-
.auto_leave_all ⇒ Object
Designed to be run periodically.
-
.clear_all! ⇒ Object
Clear all known channels.
-
.redis ⇒ Object
Shortcut to access a redis client for all PresenceChannel activities.
- .redis_eval(key, *args) ⇒ Object
-
.register_prefix(prefix, &block) ⇒ Object
Register a callback to configure channels with a given prefix Prefix must match [a-zA-Z0-9_-]+.
-
.unregister_prefix(prefix) ⇒ Object
For use in a test environment only.
Instance Method Summary collapse
-
#auto_leave ⇒ Object
Automatically expire all users which have not been ‘present’ for more than
DEFAULT_TIMEOUT
. -
#can_enter?(user_id: nil, group_ids: nil) ⇒ Boolean
Is a user allowed to enter this channel? Currently equal to the can_view? permission.
-
#can_view?(user_id: nil, group_ids: nil) ⇒ Boolean
Is this user allowed to view this channel? Pass ‘nil` for anonymous viewers.
-
#clear ⇒ Object
Clear all members of the channel.
- #count ⇒ Object
-
#initialize(name, raise_not_found: true, use_cache: true) ⇒ PresenceChannel
constructor
A new instance of PresenceChannel.
-
#leave(user_id:, client_id:) ⇒ Object
Immediately mark a user’s client as leaving the channel.
-
#present(user_id:, client_id:) ⇒ Object
Mark a user’s client as present in this channel.
-
#state(count_only: config.count_only) ⇒ Object
Fetch a State instance representing the current state of this.
- #user_ids ⇒ Object
Constructor Details
#initialize(name, raise_not_found: true, use_cache: true) ⇒ PresenceChannel
Returns a new instance of PresenceChannel.
89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/presence_channel.rb', line 89 def initialize(name, raise_not_found: true, use_cache: true) @name = name @message_bus_channel_name = "/presence#{name}" begin @config = fetch_config(use_cache: use_cache) rescue PresenceChannel::NotFound raise if raise_not_found @config = Config.new end @timeout = config.timeout || DEFAULT_TIMEOUT end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
87 88 89 |
# File 'lib/presence_channel.rb', line 87 def config @config end |
#message_bus_channel_name ⇒ Object (readonly)
Returns the value of attribute message_bus_channel_name.
87 88 89 |
# File 'lib/presence_channel.rb', line 87 def @message_bus_channel_name end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
87 88 89 |
# File 'lib/presence_channel.rb', line 87 def name @name end |
#timeout ⇒ Object (readonly)
Returns the value of attribute timeout.
87 88 89 |
# File 'lib/presence_channel.rb', line 87 def timeout @timeout end |
Class Method Details
.auto_leave_all ⇒ Object
Designed to be run periodically. Checks the channel list for channels with expired members, and runs auto_leave for each eligible channel
223 224 225 226 227 |
# File 'lib/presence_channel.rb', line 223 def self.auto_leave_all channels_with_expiring_members = PresenceChannel.redis.zrangebyscore(redis_key_channel_list, "-inf", Time.zone.now.to_i) channels_with_expiring_members.each { |name| new(name, raise_not_found: false).auto_leave } end |
.clear_all! ⇒ Object
Clear all known channels. This is intended for debugging/development only
230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/presence_channel.rb', line 230 def self.clear_all! channels = PresenceChannel.redis.zrangebyscore(redis_key_channel_list, "-inf", "+inf") channels.each { |name| new(name, raise_not_found: false).clear } config_cache_keys = PresenceChannel .redis .scan_each(match: Discourse.redis.namespace_key("_presence_*_config")) .to_a PresenceChannel.redis.del(*config_cache_keys) if config_cache_keys.present? end |
.redis ⇒ Object
Shortcut to access a redis client for all PresenceChannel activities. PresenceChannel must use the same Redis server as MessageBus, so that actions can be applied atomically. For the vast majority of Discourse installations, this is the same Redis server as ‘Discourse.redis`.
246 247 248 249 250 251 252 253 254 |
# File 'lib/presence_channel.rb', line 246 def self.redis if MessageBus.backend == :redis MessageBus.backend_instance.send(:pub_redis) # TODO: avoid a private API? elsif Rails.env.test? Discourse.redis.without_namespace else raise "PresenceChannel is unable to access MessageBus's Redis instance" end end |
.redis_eval(key, *args) ⇒ Object
256 257 258 |
# File 'lib/presence_channel.rb', line 256 def self.redis_eval(key, *args) LUA_SCRIPTS[key].eval(redis, *args) end |
.register_prefix(prefix, &block) ⇒ Object
Register a callback to configure channels with a given prefix Prefix must match [a-zA-Z0-9_-]+
For example, this registration will be used for all channels starting /topic-reply/…:
register_prefix("topic-reply") do |channel_name|
PresenceChannel::Config.new(public: true)
end
At runtime, the block will be passed a full channel name. If the channel should not exist, the block should return ‘nil`. If the channel should exist, the block should return a PresenceChannel::Config object.
Return values may be cached for up to 10 seconds.
Plugins should use the Plugin::Instance#register_presence_channel_prefix API instead
277 278 279 280 281 282 283 284 285 |
# File 'lib/presence_channel.rb', line 277 def self.register_prefix(prefix, &block) unless prefix.match? /[a-zA-Z0-9_-]+/ raise "PresenceChannel prefix #{prefix} must match [a-zA-Z0-9_-]+" end if @@configuration_blocks&.[](prefix) raise "PresenceChannel prefix #{prefix} already registered" end @@configuration_blocks[prefix] = block end |
.unregister_prefix(prefix) ⇒ Object
For use in a test environment only
288 289 290 291 |
# File 'lib/presence_channel.rb', line 288 def self.unregister_prefix(prefix) raise "Only allowed in test environment" if !Rails.env.test? @@configuration_blocks&.delete(prefix) end |
Instance Method Details
#auto_leave ⇒ Object
Automatically expire all users which have not been ‘present’ for more than DEFAULT_TIMEOUT
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/presence_channel.rb', line 196 def auto_leave mutex_value = SecureRandom.hex left_user_ids = retry_on_mutex_error do PresenceChannel.redis_eval(:auto_leave, redis_keys, [name, Time.zone.now.to_i, mutex_value]) end if !left_user_ids.empty? begin (leaving_user_ids: left_user_ids) ensure release_mutex(mutex_value) end end end |
#can_enter?(user_id: nil, group_ids: nil) ⇒ Boolean
Is a user allowed to enter this channel? Currently equal to the can_view? permission
119 120 121 122 |
# File 'lib/presence_channel.rb', line 119 def can_enter?(user_id: nil, group_ids: nil) return false if user_id.nil? can_view?(user_id: user_id, group_ids: group_ids) end |
#can_view?(user_id: nil, group_ids: nil) ⇒ Boolean
Is this user allowed to view this channel? Pass ‘nil` for anonymous viewers
105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/presence_channel.rb', line 105 def can_view?(user_id: nil, group_ids: nil) return true if config.public return true if user_id && config.allowed_user_ids&.include?(user_id) if user_id && config.allowed_group_ids.present? return true if config.allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone]) group_ids ||= GroupUser.where(user_id: user_id).pluck("group_id") return true if (group_ids & config.allowed_group_ids).present? end false end |
#clear ⇒ Object
Clear all members of the channel. This is intended for debugging/development only
213 214 215 216 217 218 219 |
# File 'lib/presence_channel.rb', line 213 def clear PresenceChannel.redis.del(redis_key_zlist) PresenceChannel.redis.del(redis_key_hash) PresenceChannel.redis.del(redis_key_config) PresenceChannel.redis.del(redis_key_mutex) PresenceChannel.redis.zrem(self.class.redis_key_channel_list, name) end |
#count ⇒ Object
191 192 193 |
# File 'lib/presence_channel.rb', line 191 def count state(count_only: true).count end |
#leave(user_id:, client_id:) ⇒ Object
Immediately mark a user’s client as leaving the channel
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/presence_channel.rb', line 150 def leave(user_id:, client_id:) mutex_value = SecureRandom.hex result = retry_on_mutex_error do PresenceChannel.redis_eval(:leave, redis_keys, [name, user_id, client_id, nil, mutex_value]) end if result == 1 begin (leaving_user_ids: [user_id]) ensure release_mutex(mutex_value) end end end |
#present(user_id:, client_id:) ⇒ Object
Mark a user’s client as present in this channel. The client_id should be unique per browser tab. This method should be called repeatedly (at least once every DEFAULT_TIMEOUT) while the user is present in the channel.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/presence_channel.rb', line 127 def present(user_id:, client_id:) raise PresenceChannel::InvalidAccess if !can_enter?(user_id: user_id) mutex_value = SecureRandom.hex result = retry_on_mutex_error do PresenceChannel.redis_eval( :present, redis_keys, [name, user_id, client_id, (Time.zone.now + timeout).to_i, mutex_value], ) end if result == 1 begin (entering_user_ids: [user_id]) ensure release_mutex(mutex_value) end end end |
#state(count_only: config.count_only) ⇒ Object
Fetch a State instance representing the current state of this
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/presence_channel.rb', line 169 def state(count_only: config.count_only) if count_only last_id, count = retry_on_mutex_error { PresenceChannel.redis_eval(:count, redis_keys) } else last_id, ids = retry_on_mutex_error { PresenceChannel.redis_eval(:user_ids, redis_keys) } end count ||= ids&.count last_id = nil if last_id == -1 if Rails.env.test? && MessageBus.backend == :memory # Doing it this way is not atomic, but we have no other option when # messagebus is not using the redis backend last_id = MessageBus.last_id() end State.new(message_bus_last_id: last_id, user_ids: ids, count: count) end |
#user_ids ⇒ Object
187 188 189 |
# File 'lib/presence_channel.rb', line 187 def user_ids state.user_ids end |