redis_queued_locks 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/project-overview.md +216 -25
  3. data/.claude/rules/acquirer.md +6 -5
  4. data/.claude/rules/arguments.md +2 -2
  5. data/.claude/rules/changelog.md +27 -0
  6. data/.claude/rules/git.md +1 -1
  7. data/.claude/rules/logic.md +10 -12
  8. data/.claude/rules/swarm.md +3 -4
  9. data/.claude/rules/tests.md +6 -6
  10. data/.claude/rules/type-checking.md +13 -14
  11. data/.claude/rules/visitors.md +5 -5
  12. data/.claude/skills/rql-code-review/SKILL.md +112 -35
  13. data/CHANGELOG.md +98 -244
  14. data/CLAUDE.md +9 -8
  15. data/README.md +275 -85
  16. data/Rakefile +11 -0
  17. data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +28 -12
  18. data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +38 -16
  19. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -15
  20. data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +430 -63
  21. data/lib/redis_queued_locks/acquirer/acquire_lock.rb +72 -22
  22. data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +240 -45
  23. data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +3 -1
  24. data/lib/redis_queued_locks/acquirer/keys.rb +6 -2
  25. data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +10 -5
  26. data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -9
  27. data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +52 -23
  28. data/lib/redis_queued_locks/acquirer/locks.rb +6 -2
  29. data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -7
  30. data/lib/redis_queued_locks/acquirer/queues.rb +17 -6
  31. data/lib/redis_queued_locks/acquirer/release_all_locks.rb +6 -2
  32. data/lib/redis_queued_locks/acquirer/release_locks_of.rb +6 -2
  33. data/lib/redis_queued_locks/client.rb +66 -24
  34. data/lib/redis_queued_locks/config.rb +93 -1
  35. data/lib/redis_queued_locks/resource.rb +124 -9
  36. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +6 -5
  37. data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
  38. data/lib/redis_queued_locks/version.rb +2 -2
  39. data/rbs_collection.lock.yaml +4 -0
  40. data/rbs_collection.yaml +2 -1
  41. data/sig/manifest.yml +1 -0
  42. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -2
  43. data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +2 -2
  44. data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +12 -6
  45. data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +21 -14
  46. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +61 -40
  47. data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +59 -11
  48. data/sig/redis_queued_locks/acquirer/acquire_lock/{yield_with_expire → yield_expire}/log_visitor.rbs +4 -4
  49. data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +2 -2
  50. data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +5 -3
  51. data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +14 -1
  52. data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +2 -2
  53. data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
  54. data/sig/redis_queued_locks/client.rbs +10 -7
  55. data/sig/redis_queued_locks/config.rbs +54 -1
  56. data/sig/redis_queued_locks/instrument/sampler.rbs +3 -1
  57. data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
  58. data/sig/redis_queued_locks/logging/sampler.rbs +3 -1
  59. data/sig/redis_queued_locks/resource.rbs +22 -12
  60. data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +1 -1
  61. data/sig/redis_queued_locks/utilities.rbs +2 -1
  62. data/sig/redis_queued_locks.rbs +7 -0
  63. metadata +4 -3
  64. /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
@@ -11,6 +11,7 @@ module RedisQueuedLocks
11
11
  RC::client redis_client,
12
12
  Integer scan_size,
13
13
  Integer dead_ttl,
14
+ bool by_priority,
14
15
  bool lock_index_enabled,
15
16
  RQL::loggerObj logger,
16
17
  RQL::instrObj instrumenter,
@@ -29,15 +30,27 @@ module RedisQueuedLocks
29
30
  RC::client redis_client,
30
31
  Integer scan_size,
31
32
  Integer dead_ttl,
33
+ bool by_priority,
32
34
  bool lock_index_enabled
33
35
  ) -> Set[String]
34
36
 
35
37
  private def self.each_lock_queue: (RC::client redis_client, Integer scan_size) { (String lock_queue) -> void } -> void
36
- private def self.drop_dead_requests: (RC::client redis_client, Array[String] lock_queues, Float dead_score) -> void
38
+ private def self.drop_dead_requests: (
39
+ RC::client redis_client,
40
+ Array[String] lock_queues,
41
+ Float dead_score,
42
+ bool by_priority
43
+ ) -> Array[Array[[String, Float]]]
44
+ private def self.drop_dead_requests_by_priority: (
45
+ RC::client redis_client,
46
+ Array[String] lock_queues,
47
+ Float dead_score
48
+ ) -> Array[Array[[String, Float]]]
37
49
  private def self.drop_dead_requests_of_locks: (
38
50
  RC::client redis_client,
39
51
  Array[String] lock_names,
40
52
  Float dead_score,
53
+ bool by_priority,
41
54
  Set[String] processed_queues
42
55
  ) -> void
43
56
  private def self.each_lock_name_batch: (RC::client redis_client, Integer scan_size) { (Array[String] lock_names) -> void } -> void
@@ -13,7 +13,7 @@ module RedisQueuedLocks
13
13
  RC::client redis_client,
14
14
  String lock_name,
15
15
  Integer milliseconds,
16
- Symbol read_write_mode,
16
+ Symbol read_write_mode, # NOTE: (rbs test) validated at runtime: any Symbol is accepted here
17
17
  bool all_read_locks,
18
18
  String acquirer_id,
19
19
  bool lock_index_enabled,
@@ -35,7 +35,7 @@ module RedisQueuedLocks
35
35
  String lock_name,
36
36
  String lock_key,
37
37
  Integer milliseconds,
38
- Symbol read_write_mode,
38
+ RQL::readWriteMode read_write_mode,
39
39
  bool all_read_locks,
40
40
  String acquirer_id,
41
41
  bool lock_index_enabled
@@ -4,7 +4,7 @@ module RedisQueuedLocks
4
4
  module Acquirer
5
5
  module Queues
6
6
  type queueList = Set[String]
7
- type queuesInfo = Set[{ queue: String, requests: Array[Hash[String,String|Float]] }]
7
+ type queuesInfo = Set[{ queue: String, requests: Array[Hash[String,String|Integer|Float]] }]
8
8
  type queues = queueList | queuesInfo
9
9
 
10
10
  def self.queues: (
@@ -44,9 +44,10 @@ module RedisQueuedLocks
44
44
  ?retry_jitter: Integer,
45
45
  ?raise_errors: bool,
46
46
  ?fail_fast: bool,
47
- ?conflict_strategy: Symbol,
48
- ?read_write_mode: :read | :write,
49
- ?access_strategy: Symbol,
47
+ ?conflict_strategy: RQL::conflictStrategy,
48
+ ?read_write_mode: RQL::readWriteMode,
49
+ ?access_strategy: RQL::accessStrategy,
50
+ ?priority: RQL::lockPriority,
50
51
  ?identity: String,
51
52
  ?meta: Hash[String|Symbol,untyped]?,
52
53
  ?detailed_acq_timeout_error: bool,
@@ -75,9 +76,10 @@ module RedisQueuedLocks
75
76
  ?retry_delay: Integer,
76
77
  ?retry_jitter: Integer,
77
78
  ?fail_fast: bool,
78
- ?conflict_strategy: Symbol,
79
- ?read_write_mode: :read | :write,
80
- ?access_strategy: Symbol,
79
+ ?conflict_strategy: RQL::conflictStrategy,
80
+ ?read_write_mode: RQL::readWriteMode,
81
+ ?access_strategy: RQL::accessStrategy,
82
+ ?priority: RQL::lockPriority,
81
83
  ?identity: String,
82
84
  ?instrumenter: RQL::instrObj,
83
85
  ?meta: Hash[String|Symbol,untyped]?,
@@ -158,7 +160,7 @@ module RedisQueuedLocks
158
160
  def extend_lock_ttl: (
159
161
  String lock_name,
160
162
  Integer milliseconds,
161
- ?read_write_mode: :read | :write,
163
+ ?read_write_mode: RQL::readWriteMode,
162
164
  ?all_read_locks: bool,
163
165
  ?identity: String,
164
166
  ?logger: RQL::loggerObj,
@@ -242,6 +244,7 @@ module RedisQueuedLocks
242
244
  def clear_dead_requests: (
243
245
  ?dead_ttl: Integer,
244
246
  ?scan_size: Integer,
247
+ ?by_priority: bool,
245
248
  ?logger: RQL::loggerObj,
246
249
  ?instrumenter: RQL::instrObj,
247
250
  ?instrument: untyped?,
@@ -7,11 +7,60 @@ module RedisQueuedLocks
7
7
  type configState = Hash[String,untyped]
8
8
 
9
9
  @config_state: configState
10
+ @configuring: bool
10
11
  attr_reader config_state: configState
11
12
 
12
13
  def initialize: () ?{ (RedisQueuedLocks::Config) -> void } -> void
13
14
  def configure: () ?{ (RedisQueuedLocks::Config) -> void } -> void
14
- def []: (String config_key) -> untyped
15
+ # NOTE: (typed config lookups) one overload per config key (the `setting` list of `config.rb`):
16
+ # Steep checks `config['...']` defaults in `Client` and rejects unknown keys;
17
+ # keep this table in sync with `setting`/`validate`;
18
+ def []: ("retry_count") -> Integer?
19
+ | ("retry_delay") -> Integer
20
+ | ("retry_jitter") -> Integer
21
+ | ("try_to_lock_timeout") -> Integer?
22
+ | ("default_lock_ttl") -> Integer
23
+ | ("default_queue_ttl") -> Integer
24
+ | ("detailed_acq_timeout_error") -> bool
25
+ | ("lock_release_batch_size") -> Integer
26
+ | ("clear_locks_of__lock_scan_size") -> Integer
27
+ | ("clear_locks_of__queue_scan_size") -> Integer
28
+ | ("key_extraction_batch_size") -> Integer
29
+ | ("instrumenter") -> RedisQueuedLocks::instrObj
30
+ | ("uniq_identifier") -> ^() -> String
31
+ | ("logger") -> RedisQueuedLocks::loggerObj
32
+ | ("log_lock_try") -> bool
33
+ | ("dead_request_ttl") -> Integer
34
+ | ("lock_index_enabled") -> bool
35
+ | ("lock_index_grace_period") -> Integer
36
+ | ("is_timed_by_default") -> bool
37
+ | ("default_conflict_strategy") -> RedisQueuedLocks::conflictStrategy
38
+ | ("default_access_strategy") -> RedisQueuedLocks::accessStrategy
39
+ | ("lock_priorities") -> Hash[Symbol, Integer]
40
+ | ("default_lock_priority") -> RedisQueuedLocks::lockPriority
41
+ | ("log_sampling_enabled") -> bool
42
+ | ("log_sampling_percent") -> Integer
43
+ | ("log_sampler") -> RedisQueuedLocks::Logging::samplerObj
44
+ | ("instr_sampling_enabled") -> bool
45
+ | ("instr_sampling_percent") -> Integer
46
+ | ("instr_sampler") -> RedisQueuedLocks::Instrument::samplerObj
47
+ | ("swarm.auto_swarm") -> bool
48
+ | ("swarm.supervisor.liveness_probing_period") -> Integer
49
+ | ("swarm.probe_hosts.enabled_for_swarm") -> bool
50
+ | ("swarm.probe_hosts.probe_period") -> Integer
51
+ | ("swarm.probe_hosts.redis_config.sentinel") -> bool
52
+ | ("swarm.probe_hosts.redis_config.pooled") -> bool
53
+ | ("swarm.probe_hosts.redis_config.config") -> Hash[untyped,untyped]
54
+ | ("swarm.probe_hosts.redis_config.pool_config") -> Hash[untyped,untyped]
55
+ | ("swarm.flush_zombies.enabled_for_swarm") -> bool
56
+ | ("swarm.flush_zombies.zombie_ttl") -> Integer
57
+ | ("swarm.flush_zombies.zombie_lock_scan_size") -> Integer
58
+ | ("swarm.flush_zombies.zombie_queue_scan_size") -> Integer
59
+ | ("swarm.flush_zombies.zombie_flush_period") -> Integer
60
+ | ("swarm.flush_zombies.redis_config.sentinel") -> bool
61
+ | ("swarm.flush_zombies.redis_config.pooled") -> bool
62
+ | ("swarm.flush_zombies.redis_config.config") -> Hash[untyped,untyped]
63
+ | ("swarm.flush_zombies.redis_config.pool_config") -> Hash[untyped,untyped]
15
64
  def []=: (String config_key, untyped config_value) -> void
16
65
  def slice: (String config_key_pattern) -> configState
17
66
 
@@ -19,5 +68,9 @@ module RedisQueuedLocks
19
68
 
20
69
  def prevent_key__non_existent: (String config_key) -> void
21
70
  def prevent_key__invalid_type: (String config_key, untyped config_value) -> void
71
+ def prevent_default_priority__unknown_name: (
72
+ Hash[Symbol, Integer] lock_priorities,
73
+ RedisQueuedLocks::lockPriority default_lock_priority
74
+ ) -> void
22
75
  end
23
76
  end
@@ -1,7 +1,9 @@
1
1
  module RedisQueuedLocks
2
2
  module Instrument
3
3
  module Sampler
4
- def self.sampling_happened?: (Integer sampling_percent) -> bool
4
+ # NOTE: (rbs test) not hooked: `valid_sampler?` introspects `#parameters` of this method,
5
+ # and a hooked method has the `(*rest, &block)` parameters;
6
+ %a{rbs:test:skip} def self.sampling_happened?: (Integer sampling_percent) -> bool
5
7
  end
6
8
  end
7
9
  end
@@ -1,7 +1,9 @@
1
1
  module RedisQueuedLocks
2
2
  module Instrument
3
3
  module VoidNotifier
4
- def self.notify: (String event, ?Hash[untyped,untyped] payload) -> void
4
+ # NOTE: (rbs test) not hooked: `Instrument.valid_interface?` introspects `#parameters` of `notify`,
5
+ # and a hooked method has the `(*rest, &block)` parameters;
6
+ %a{rbs:test:skip} def self.notify: (String event, ?Hash[untyped,untyped] payload) -> void
5
7
  end
6
8
  end
7
9
  end
@@ -1,7 +1,9 @@
1
1
  module RedisQueuedLocks
2
2
  module Logging
3
3
  module Sampler
4
- def self.sampling_happened?: (Integer sampling_percent) -> bool
4
+ # NOTE: (rbs test) not hooked: `valid_sampler?` introspects `#parameters` of this method,
5
+ # and a hooked method has the `(*rest, &block)` parameters;
6
+ %a{rbs:test:skip} def self.sampling_happened?: (Integer sampling_percent) -> bool
5
7
  end
6
8
  end
7
9
  end
@@ -10,6 +10,9 @@ module RedisQueuedLocks
10
10
  SWARM_KEY: String
11
11
  LOCK_INDEX_KEY: String
12
12
  REDIS_TIMESHIFT_ERROR: Integer
13
+ MIN_LOCK_PRIORITY: Integer
14
+ MAX_LOCK_PRIORITY: Integer
15
+ LOCK_PRIORITY_BAND: Integer
13
16
 
14
17
  def self.calc_uniq_identity: () -> String
15
18
  def self.acquirer_identifier: (
@@ -27,24 +30,31 @@ module RedisQueuedLocks
27
30
  String identity
28
31
  ) -> String
29
32
 
30
- def self.prepare_lock_key: (String lock_name) -> String
31
- def self.prepare_lock_queue: (String lock_name) -> String
32
- def self.prepare_read_lock_queue: (String lock_name) -> String
33
- def self.prepare_lock_readers: (String lock_name) -> String
34
- def self.prepare_read_lock_key: (String lock_name, String acquirer_id) -> String
33
+ # NOTE: (rbs test) `%a{rbs:test:skip}`: these functions run inside the zombie flusher Ractor
34
+ # (`FlushZombies.flush_zombies`); rbs/test hooks read `RBS.logger` and raise
35
+ # `Ractor::IsolationError` there, so they are not hooked (Steep still checks them).
36
+ %a{rbs:test:skip} def self.prepare_lock_key: (String lock_name) -> String
37
+ %a{rbs:test:skip} def self.prepare_lock_queue: (String lock_name) -> String
38
+ %a{rbs:test:skip} def self.prepare_read_lock_queue: (String lock_name) -> String
39
+ %a{rbs:test:skip} def self.prepare_lock_readers: (String lock_name) -> String
40
+ %a{rbs:test:skip} def self.prepare_read_lock_key: (String lock_name, String acquirer_id) -> String
35
41
  def self.lock_queue_rw_mode: (String lock_queue) -> String?
36
42
  type lockKeyFamily = :lock | :lock_queue | :lock_readers | :read_lock_queue | :read_lock
37
- def self.lock_key_family: (String key) -> lockKeyFamily?
43
+ %a{rbs:test:skip} def self.lock_key_family: (String key) -> lockKeyFamily?
38
44
  def self.lock_name_from_key: (String key, lockKeyFamily? key_family) -> String?
39
- def self.lock_index_time: () -> Integer
40
- def self.lock_name_from_readers: (String lock_readers_key) -> String
45
+ %a{rbs:test:skip} def self.lock_index_time: () -> Integer
46
+ def self.lock_index_score_expired?: (String lock_index_score, Integer lock_index_time) -> bool
47
+ %a{rbs:test:skip} def self.lock_name_from_readers: (String lock_readers_key) -> String
41
48
  def self.lock_key_from_readers: (String lock_readers_key) -> String
42
- def self.host_identifier_from_acquirer: (String acquirer_id) -> String
49
+ %a{rbs:test:skip} def self.host_identifier_from_acquirer: (String acquirer_id) -> String
43
50
  def self.redis_time_ms: (Array[String] redis_time) -> Integer
44
- def self.calc_initial_acquirer_position: () -> Float
51
+ def self.calc_initial_acquirer_position: (Integer priority) -> Float
45
52
  def self.acquirer_dead_score: (Integer|Float queue_ttl) -> Float
46
- def self.calc_zombie_score: (Float zombie_ttl) -> Float
47
- def self.dead_score_reached?: (Float acquirer_position, Integer queue_ttl) -> bool
53
+ def self.lock_priority_band_floor: (Integer priority) -> Integer
54
+ def self.lock_priority_band_dead_score: (Float dead_score, Integer priority) -> Float
55
+ def self.lock_request_priority: (Integer|Float acquirer_position) -> Integer
56
+ %a{rbs:test:skip} def self.calc_zombie_score: (Float zombie_ttl) -> Float
57
+ def self.dead_score_reached?: (Float acquirer_position, Integer priority, Integer queue_ttl) -> bool
48
58
  def self.lock_key_from_queue: (String lock_queue) -> String?
49
59
  def self.get_thread_id: () -> Integer
50
60
  def self.get_fiber_id: () -> Integer
@@ -54,7 +54,7 @@ module RedisQueuedLocks
54
54
  def swarm_loop__start: () -> void
55
55
  def swarm_loop__stop: () -> void
56
56
  def swarm_loop__send_command: (Symbol command) -> swarmLoopReply?
57
- def swarm_element__termiante: () -> void
57
+ def swarm_element__terminate: () -> void
58
58
  end
59
59
  end
60
60
  end
@@ -7,7 +7,8 @@ module RedisQueuedLocks
7
7
  def self?.run_non_critical: () { (?) -> untyped } -> untyped?
8
8
  def self?.ractor_status: (Ractor ractor) -> String
9
9
  def self?.ractor_alive?: (Ractor ractor) -> bool
10
- def self?.thread_state: (Thread thread) -> String
10
+ # NOTE: (rbs test) runs inside the element Ractor (`Isolated.swarm_loop`), see `resource.rbs`;
11
+ %a{rbs:test:skip} def self?.thread_state: (Thread thread) -> String
11
12
  def self?.clock_gettime: () -> Integer
12
13
  def self?.ruby_over_4?: () -> bool
13
14
  end
@@ -10,5 +10,12 @@ module RedisQueuedLocks
10
10
  type loggerObj = Object & _Loggable
11
11
  type instrObj = Object & _Instrumentable
12
12
 
13
+ type readWriteMode = :read | :write
14
+ type accessStrategy = :queued | :random
15
+ type conflictStrategy = :wait_for_lock | :work_through | :extendable_work_through | :dead_locking
16
+ # NOTE: (lock request priorities) an Integer priority or a priority name of
17
+ # `config['lock_priorities']` (resolved by `AcquireLock.acquire_lock`);
18
+ type lockPriority = Integer | Symbol
19
+
13
20
  extend ::RedisQueuedLocks::Debugger::Interface
14
21
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis_queued_locks
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.21.0
4
+ version: 1.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rustam Ibragimov
@@ -38,6 +38,7 @@ files:
38
38
  - ".claude/project-overview.md"
39
39
  - ".claude/rules/acquirer.md"
40
40
  - ".claude/rules/arguments.md"
41
+ - ".claude/rules/changelog.md"
41
42
  - ".claude/rules/git.md"
42
43
  - ".claude/rules/logic.md"
43
44
  - ".claude/rules/swarm.md"
@@ -122,7 +123,7 @@ files:
122
123
  - redis_queued_locks.gemspec
123
124
  - sig/manifest.yml
124
125
  - sig/redis_queued_locks.rbs
125
- - sig/redis_queued_locks/acquier.rbs
126
+ - sig/redis_queued_locks/acquirer.rbs
126
127
  - sig/redis_queued_locks/acquirer/acquire_lock.rbs
127
128
  - sig/redis_queued_locks/acquirer/acquire_lock/delay_execution.rbs
128
129
  - sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs
@@ -133,7 +134,7 @@ files:
133
134
  - sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs
134
135
  - sig/redis_queued_locks/acquirer/acquire_lock/with_acq_timeout.rbs
135
136
  - sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs
136
- - sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs
137
+ - sig/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rbs
137
138
  - sig/redis_queued_locks/acquirer/clear_dead_requests.rbs
138
139
  - sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs
139
140
  - sig/redis_queued_locks/acquirer/is_locked.rbs