hubssolib 3.4.0 → 3.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1a83a70b87a6c3cabd8bbb821dfc06ee467d382c9593cb7c1733152e136f5a1
4
- data.tar.gz: 87f1dda92220f25326c8bbce597ecd97e74c3dd7e22fd0fbf89bfed6269b6b23
3
+ metadata.gz: 8fc6eb926595d43f0fe51803fb2dd817930adf4385a742461c13197c87ebd8a5
4
+ data.tar.gz: 3f532eca169d7497d595aeb15a31d47650ee13fd4973859041d40f66144217cc
5
5
  SHA512:
6
- metadata.gz: cf2ae3afb91a4fcc3391cf4b156e3dd9e4d0be4cecafc10204cf6dc8a7d6b9d07d51dc08cd7fd22ee12a478025d03730227fa2602a194d4b0e6785d72af21295
7
- data.tar.gz: 6ac8d8a0a1920d7f93604204ef0c254a094bd4a8559110bba2f630c1fe406f87966b0ba2289d017a9a48e90cd0d6e2eab0e1ae3872032c7bad389942965fe2b2
6
+ metadata.gz: 1c645f57251251d560c8940d25aaaade1e98b83fe7cf8ab0dff194c3738bfb0c0e8699a1aed7c4889b656e7c0da81f005722773c1af52205eb233f7f9b7ef3dc
7
+ data.tar.gz: 6927b5bd57c5d5c7eddb65cb36c20fc89f24d6cf8ac1ad341da2feedf4ae6e12f452351416d089eb046214a4895d2e7becf7c098bb4c1cd0c011bf8eaafd36c2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 3.5.0, 25-Mar-2025
2
+
3
+ Builds on the cleaner session interface with some changes and improvements:
4
+
5
+ * Removed the 'clean up other sessions under that user ID' code, since that stopped you being able to log in on more than one browser - notably, both e.g. a desktop/laptop computer's browser and a mobile device's browser. This does carry a risk of stale session cookies for a user building up over time, but the sweeper Rake task will attend to those in due course.
6
+ * On the other hand the Hub core explicitly say whether or not it is trying to read or create a session, so the internal lazy-create no longer triggers for outdated session cookies, resulting in unnecessary "empty" sessions where all the user properties are "nil". This prevents accumulation of sessions for some cases.
7
+
8
+ Minor version number arises due to a new feature:
9
+
10
+ * If the DRb server receives signals `TERM` (e.g. default `kill`) or `INT` (e.g. `Ctrl`+`C`) it now gracefully shuts down, dumping user sessions to a YAML file in a location of `~/.hub_ses_arc` by default, or the filename in environment variable `HUB_SESSION_ARCHIVE`, if defined.
11
+ * On startup, this file is loaded. If exceptions are encountered they are ignored and session data is ignored, leading to a clean start. Likewise simply deleting the file leads to a clean start - otherwise, it becomes possible to cycle the DRb server and retain session data! **A self-sweep of ancient sessions is conducted at startup**, which takes the same action as the `hub_sessions:sweep` Rake task.
12
+ * Once the server is running the file is deleted, so in case of hard crash or forced exit via other signals (e.g. `kill -9`) - so a new session dump is _not_ made - then session data is lost, by design. The assumption in such circumstances is that session data is unreliable and may even have been the cause of a crash.
13
+
1
14
  ## 3.4.0, 24-Mar-2025
2
15
 
3
16
  The session interface has now been cleaned up. While the public API is unchanged, you:
data/README.md CHANGED
@@ -208,6 +208,8 @@ User-idle session expiry is routinely handled in the "beforehand" callback so th
208
208
 
209
209
  Users who log in but then go idle will not cause any natural session expiry by virtue of there being no new page fetch activity provoking the idle timeout check. For users who've just gone away full stop, this means a session record is left hanging around in DRb server memory (or if future iterations support other storage methods, they'd persist in whatever storage that is). For this reason, the Hub _application_ provides a Rake task which sweeps sessions based on things idle for three times the `HUB_IDLE_TIME_LIMIT`, or two days, whichever is longer (so short idle timers still give users a couple of days to potentially come back to their old session and get the "nice" expiry message). Run `bundle exec rake hub_sessions:sweep` whenever you want (in practice, once a day is enough). **Be sure to set up any custom Hub environment variables for your setup** so that the Rake task knows where to find the DRb socket for the running server, what your expiry time is if so customised, and so-on.
210
210
 
211
+ * **Note:** The DRb server persists sessions upon graceful shutdown (`INT` or `TERM` signals) into file `~/.hub_ses_arc` by default, or the filename in environment variable `HUB_SESSION_ARCHIVE` if defined. When the server restarts, it loads this data **and runs the same very old session expiry** algorithm. This means that shutting down and restarting the DRb server is another way to expire very old sessions, but that results in downtime, even if only brief; so for general maintenance with uptime maintained, the Rake task should be used.
212
+
211
213
 
212
214
 
213
215
  ## Hub library API
data/hubssolib.gemspec CHANGED
@@ -4,7 +4,7 @@ spec = Gem::Specification.new do |s|
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.name = 'hubssolib'
6
6
 
7
- s.version = '3.4.0'
7
+ s.version = '3.5.0'
8
8
  s.author = 'Andrew Hodgkinson and others'
9
9
  s.email = 'ahodgkin@rowing.org.uk'
10
10
  s.homepage = 'http://pond.org.uk/'
data/lib/hub_sso_lib.rb CHANGED
@@ -20,8 +20,10 @@ module HubSsoLib
20
20
  require 'drb'
21
21
  require 'securerandom'
22
22
  require 'json'
23
+ require 'yaml'
23
24
 
24
- # DRb connection
25
+ # DRb connection.
26
+ #
25
27
  HUB_CONNECTION_URI = ENV['HUB_CONNECTION_URI'] || 'drbunix:' + File.join( ENV['HOME'] || '/', '/.hub_drb')
26
28
 
27
29
  unless HUB_CONNECTION_URI.downcase.start_with?('drbunix:')
@@ -34,7 +36,8 @@ module HubSsoLib
34
36
  raise 'Exiting'
35
37
  end
36
38
 
37
- # External application command registry for on-user-change events
39
+ # External application command registry for on-user-change events.
40
+ #
38
41
  HUB_COMMAND_REGISTRY = ENV['HUB_COMMAND_REGISTRY'] || File.join( ENV['HOME'] || '/', '/.hub_cmd_reg')
39
42
 
40
43
  unless Dir.exist?(File.dirname(HUB_COMMAND_REGISTRY))
@@ -47,6 +50,20 @@ module HubSsoLib
47
50
  raise 'Exiting'
48
51
  end
49
52
 
53
+ # DRb session on-shutdown dumped cache/archive.
54
+ #
55
+ HUB_SESSION_ARCHIVE = ENV['HUB_SESSION_ARCHIVE'] || File.join( ENV['HOME'] || '/', '/.hub_ses_arc')
56
+
57
+ unless Dir.exist?(File.dirname(HUB_SESSION_ARCHIVE))
58
+ puts
59
+ puts '*' * 80
60
+ puts "Invalid path specified by HUB_SESSION_ARCHIVE (#{ HUB_SESSION_ARCHIVE.inspect })"
61
+ puts '*' * 80
62
+ puts
63
+
64
+ raise 'Exiting'
65
+ end
66
+
50
67
  # Location of Hub application root.
51
68
  #
52
69
  HUB_PATH_PREFIX = ENV['HUB_PATH_PREFIX'] || ''
@@ -438,7 +455,33 @@ module HubSsoLib
438
455
  @hub_be_quiet = ! ENV['HUB_QUIET_SERVER'].nil?
439
456
  @hub_sessions = {}
440
457
 
441
- puts "Session factory: Awaken" unless @hub_be_quiet
458
+ puts "Session factory: Awakening..." unless @hub_be_quiet
459
+
460
+ if File.exist?(HUB_SESSION_ARCHIVE)
461
+ begin
462
+ restored_sessions = ::YAML.load_file(
463
+ HUB_SESSION_ARCHIVE,
464
+ permitted_classes: [
465
+ ::HubSsoLib::Session,
466
+ ::HubSsoLib::User,
467
+ Time
468
+ ]
469
+ )
470
+
471
+ @hub_sessions = restored_sessions || {}
472
+ self.destroy_ancient_sessions()
473
+ puts "Session factory: Reloaded #{@hub_sessions.keys.size} from archive" unless @hub_be_quiet
474
+
475
+ rescue => e
476
+ puts "Session factory: Ignored archive due to error #{e.message.inspect}" unless @hub_be_quiet
477
+
478
+ ensure
479
+ File.unlink(HUB_SESSION_ARCHIVE)
480
+
481
+ end
482
+ end
483
+
484
+ puts "Session factory: ...Awakened" unless @hub_be_quiet
442
485
 
443
486
  rescue => e
444
487
  Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
@@ -457,17 +500,27 @@ module HubSsoLib
457
500
  # The returned object is proxied via DRb - it is shared between processes.
458
501
  #
459
502
  # +key+:: Session key; lazy-initialises a new session under this key
460
- # if none is found, then immediately rotates it.
503
+ # if none is found, then immediately rotates it by default,
504
+ # but may return no session for unrecognised keys depending
505
+ # on the +create+ parameter, described below.
461
506
  #
462
507
  # +remote_ip+:: Request's remote IP address. If there is an existing
463
508
  # session which matches this, it's returned. If there is an
464
509
  # existing session but the IP mismatches, it's treated as
465
510
  # invalid and discarded.
466
511
  #
467
- def get_hub_session_proxy(key, remote_ip)
512
+ # In addition, the following optional named parameters can be given:
513
+ #
514
+ # +create+:: Default +true+ - an unknown key causes creation of an empty,
515
+ # new session under that key. If +false+, attempts to read with
516
+ # an unrecognised key yield +nil+.
517
+ #
518
+ def get_hub_session_proxy(key, remote_ip, create: true)
468
519
  hub_session = HUB_MUTEX.synchronize { @hub_sessions[key] }
469
- message = hub_session.nil? ? 'Created' : 'Retrieving'
470
- new_key = SecureRandom.uuid
520
+ return nil if create == false && hub_session.nil? # NOTE EARLY EXIT
521
+
522
+ message = hub_session.nil? ? 'Created' : 'Retrieving'
523
+ new_key = SecureRandom.uuid
471
524
 
472
525
  unless @hub_be_quiet
473
526
  puts "Session factory: #{ message } session for key #{ key } and rotating to #{ new_key }"
@@ -606,6 +659,41 @@ module HubSsoLib
606
659
  raise
607
660
  end
608
661
 
662
+ # Lock the session store and dump all sessions with a non-nil session user
663
+ # ID to a YAML file at HUB_SESSION_ARCHIVE. This is expected to only be
664
+ # called by the graceful shutdown code in HubSsoLib::Server.
665
+ #
666
+ def dump_sessions!
667
+ written_record_count = 0
668
+
669
+ # Why not just do ::YAML.dump(@hub_sessions)? Well, it'd be faster, but
670
+ # it builds the YAML data all in RAM which would cause a huge RAM spike
671
+ # of unknown size (depends on live session count) and that's Bad.
672
+ #
673
+ # If any no-user sessions have crept in for any reason, this also gives
674
+ # us a chance to skip them.
675
+ #
676
+ HUB_MUTEX.synchronize do
677
+ File.open(HUB_SESSION_ARCHIVE, 'w') do | f |
678
+ f.write("---\n") # (document marker)
679
+
680
+ @hub_sessions.each do | key, session |
681
+ next if session&.session_user&.user_id.nil? # NOTE EARLY LOOP RESTART
682
+
683
+ dump = ::YAML.dump({key => session})
684
+ dump.sub!(/^---\n/, '') # (avoid multiple document markers)
685
+
686
+ f.write(dump)
687
+ written_record_count += 1
688
+ end
689
+ end
690
+ end
691
+
692
+ # Simple if slightly inefficient way to deal with zero actual useful
693
+ # session records being present - an unusual real-world edge case.
694
+ #
695
+ File.unlink(HUB_SESSION_ARCHIVE) if written_record_count == 0
696
+ end
609
697
  end
610
698
 
611
699
  #######################################################################
@@ -626,16 +714,41 @@ module HubSsoLib
626
714
 
627
715
  module Server
628
716
  def hubssolib_launch_server
629
- puts "Server: Starting at #{ HUB_CONNECTION_URI }" unless ENV['HUB_QUIET_SERVER'].nil?
717
+ ::HubSsoLib::Server::Runner.run
718
+ end
630
719
 
631
- @@hub_session_factory = HubSsoLib::SessionFactory.new
632
- DRb.start_service(HUB_CONNECTION_URI, @@hub_session_factory, { :safe_level => 1 })
633
- DRb.thread.join
720
+ class Runner
721
+ QUEUE = ::Queue.new
634
722
 
635
- rescue => e
636
- Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
637
- raise
638
- end
723
+ def self.run
724
+ puts "Server: Starting at #{ HUB_CONNECTION_URI }" if ENV['HUB_QUIET_SERVER'].nil?
725
+
726
+ @@hub_session_factory = HubSsoLib::SessionFactory.new
727
+
728
+ Signal.trap('INT' ) { QUEUE << :INT }
729
+ Signal.trap('TERM') { QUEUE << :TERM }
730
+
731
+ DRb.start_service(HUB_CONNECTION_URI, @@hub_session_factory, { :safe_level => 1 })
732
+
733
+ QUEUE.pop
734
+
735
+ self.shutdown()
736
+ exit
737
+
738
+ rescue => e
739
+ Sentry.capture_exception(e) if defined?(Sentry) && Sentry.respond_to?(:capture_exception)
740
+ raise
741
+ end
742
+
743
+ def self.shutdown
744
+ puts "Server: Graceful shutdown..."
745
+
746
+ @@hub_session_factory.dump_sessions!
747
+ DRb.stop_service
748
+
749
+ puts "Server: ...completed."
750
+ end
751
+ end # Runner class
639
752
  end # Server module
640
753
 
641
754
  #######################################################################
@@ -701,13 +814,6 @@ module HubSsoLib
701
814
  if user.nil?
702
815
  self.hubssolib_destroy_session!
703
816
  else
704
-
705
- # Stale sessions may exist for this user. The frequency of logins is
706
- # very low compared to frequency of page requests, so we perform this
707
- # expensive method here as a least-worst option!
708
- #
709
- hubssolib_factory().destroy_sessions_by_user_id(user.user_id)
710
-
711
817
  hub_session = self.hubssolib_create_session()
712
818
  hub_session.session_user = user
713
819
  end
@@ -1231,7 +1337,7 @@ module HubSsoLib
1231
1337
  self.hubssolib_destroy_session! unless old_session.nil?
1232
1338
 
1233
1339
  start_key = SecureRandom.uuid
1234
- @hubssolib_session = hubssolib_factory().get_hub_session_proxy(start_key, request.remote_ip)
1340
+ @hubssolib_session = hubssolib_factory().get_hub_session_proxy(start_key, request.remote_ip, create: true)
1235
1341
 
1236
1342
  # The session is now stored under the rotated key, so put that into the
1237
1343
  # Hub session cookie so that on the next request, we can retrieve the
@@ -1263,10 +1369,12 @@ module HubSsoLib
1263
1369
  if @hubssolib_session.nil?
1264
1370
  key = cookies[HUB_COOKIE_NAME]
1265
1371
 
1266
- if key.nil? || key == ''
1372
+ # See #hubssolib_create_session - valid keys are 36-char UUIDs
1373
+ #
1374
+ if key.nil? || key.size != 36
1267
1375
  @hubssolib_session = nil
1268
1376
  else
1269
- hub_session = hubssolib_factory().get_hub_session_proxy(key, request.remote_ip)
1377
+ hub_session = hubssolib_factory().get_hub_session_proxy(key, request.remote_ip, create: false)
1270
1378
 
1271
1379
  if hub_session.nil? # Invalid key in cookie
1272
1380
  @hubssolib_session = nil
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hubssolib
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Hodgkinson and others
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-24 00:00:00.000000000 Z
10
+ date: 2025-03-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: drb