Class: IbmAppconfigurationRubySdk::ConfigurationHandler

Inherits:
Object
  • Object
show all
Includes:
Utils, Singleton
Defined in:
lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils

append_segment_id, compute_hash, extract_configurations, extract_environment_data, extract_resources, get_current_rollout_percentage, get_normalized_value, parse_rollout_configuration_phases, symbolize_keys, validate_resource

Constructor Details

#initializeConfigurationHandler

Returns a new instance of ConfigurationHandler.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 51

def initialize
  @collection_id = nil
  @environment_id = nil
  @guid = nil
  @connected = true
  @live_update = true
  @bootstrap_file = nil
  @persistent_cache_directory = nil

  @feature_map = {}
  @property_map = {}
  @segment_map = {}
  @secret_map = {}
  @rollout_config_map = {}
  @all_feature_flags = []
  @cache_mutex = Mutex.new

  @logger = Logger.instance
  @file_manager = FileManager.instance
  @websocket_client = nil

  # Configuration update listener (single listener, matches Java SDK)
  @configuration_update_listener = nil
end

Class Method Details

.current_instanceObject

Returns the existing singleton without creating a new one. Raises ConfigurationError if called before the handler has been initialised — matching Node SDK's configurationHandler.currentInstance() behaviour.



44
45
46
47
48
49
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 44

def self.current_instance
  raise IbmAppconfigurationRubySdk::ConfigurationError, Constants::SINGLETON_EXCEPTION \
    unless @singleton__instance__

  instance
end

Instance Method Details

#cleanupObject

Cleanup resources



105
106
107
108
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 105

def cleanup
  Metering.instance.cleanup
  @websocket_client&.disconnect
end

#connected?Boolean

Check if connected

Returns:

  • (Boolean)

    Connection status



762
763
764
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 762

def connected?
  @connected
end

#evaluate_rules(rules_map, entity_attributes, feature, property, entity_id = nil) ⇒ Hash

Evaluate rules

Parameters:

  • rules_map (Hash)

    Rules map

  • entity_attributes (Hash)

    Entity attributes

  • feature (Feature, nil)

    Feature object

  • property (Property, nil)

    Property object

  • entity_id (String) (defaults to: nil)

    Entity ID

Returns:

  • (Hash)

    Evaluation result



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 519

def evaluate_rules(rules_map, entity_attributes, feature, property, entity_id = nil)
  result_dict = {
    evaluated_segment_id: Constants::DEFAULT_SEGMENT_ID,
    value: nil,
    enabled: false,
    details: {}
  }

  begin
    # For each rule in the targeting
    (1..rules_map.keys.length).each do |index|
      segment_rule = rules_map[index]

      next unless segment_rule.get_rules.length.positive?

      segment_rule.get_rules.each do |rule|
        segments = rule[:segments]

        next unless segments&.length&.positive?

        # For each segment in a rule
        segments.each do |segment_key|
          # Check whether the entityAttributes satisfies all the rules of that segment
          next unless evaluate_segment(segment_key, entity_attributes)

          segment_obj = get_segment(segment_key)
          segment_name = segment_obj&.name
          result_dict[:evaluated_segment_id] = segment_key
          result_dict[:details][:segment_name] = segment_name

          if feature
            # evaluateRules was called for feature flag
            segment_level_rollout_percentage, effective_entity_id = get_rollout_percentage(feature, segment_rule, entity_id)

            # Check whether the entityId is eligible for segment rollout
            if segment_level_rollout_percentage == 100 ||
               (entity_id && get_normalized_value("#{effective_entity_id}:#{feature.feature_id}") < segment_level_rollout_percentage)
              # Since the entityId is eligible for segment rollout, return inherited or overridden value
              result_dict[:value] = if segment_rule.get_value == Constants::DEFAULT_FEATURE_VALUE
                                      feature.enabled_value # Return the inherited value
                                    else
                                      segment_rule.get_value # Return the overridden value
                                    end
              result_dict[:details][:value_type] = "SEGMENT_VALUE"
              result_dict[:enabled] = true
              result_dict[:details][:rollout_percentage_applied] = true
            else
              result_dict[:value] = feature.disabled_value
              result_dict[:enabled] = false
              result_dict[:details][:value_type] = "DISABLED_VALUE"
              result_dict[:details][:rollout_percentage_applied] = false
            end
          else
            # evaluateRules was called for property
            result_dict[:value] = if segment_rule.get_value == Constants::DEFAULT_PROPERTY_VALUE
                                    property.value
                                  else
                                    segment_rule.get_value
                                  end
            result_dict[:details][:value_type] = "SEGMENT_VALUE"
          end
          return result_dict
        end
      end
    end
  rescue StandardError => e
    @logger.error("RuleEvaluation #{e}")
    result_dict[:value] = nil
    result_dict[:enabled] = false
    result_dict[:details][:value_type] = "ERROR"
    result_dict[:details][:error_type] = e.message
    return result_dict
  end

  # Since entityAttributes did not satisfy any of the targeting rules
  if feature
    # evaluateRules was called for feature flag
    # Check whether the entityId is eligible for default rollout
    rollout_percentage, effective_entity_id = get_rollout_percentage(feature, nil, entity_id)

    if rollout_percentage == 100 ||
       (entity_id && get_normalized_value("#{effective_entity_id}:#{feature.feature_id}") < rollout_percentage)
      result_dict[:value] = feature.enabled_value
      result_dict[:enabled] = true
      result_dict[:details][:value_type] = "ENABLED_VALUE"
      result_dict[:details][:rollout_percentage_applied] = true
    else
      result_dict[:value] = feature.disabled_value
      result_dict[:enabled] = false
      result_dict[:details][:value_type] = "DISABLED_VALUE"
      result_dict[:details][:rollout_percentage_applied] = false
    end
  else
    # evaluateRules was called for property
    result_dict[:value] = property.value
    result_dict[:details][:value_type] = "DEFAULT_VALUE"
  end

  result_dict
end

#evaluate_segment(segment_key, entity_attributes) ⇒ Boolean

Evaluate segment

Parameters:

  • segment_key (String)

    Segment key

  • entity_attributes (Hash)

    Entity attributes

Returns:

  • (Boolean)

    Evaluation result



437
438
439
440
441
442
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 437

def evaluate_segment(segment_key, entity_attributes)
  segment_obj = @cache_mutex.synchronize { @segment_map[segment_key] }
  return segment_obj.evaluate_rule(entity_attributes) if segment_obj

  nil
end

#feature_evaluation(feature, entity_id, entity_attributes) ⇒ EvaluationResult

Feature evaluation

Parameters:

  • feature (Feature)

    Feature object

  • entity_id (String)

    Entity ID

  • entity_attributes (Hash)

    Entity attributes

Returns:



644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 644

def feature_evaluation(feature, entity_id, entity_attributes)
  result_dict = {
    evaluated_segment_id: Constants::DEFAULT_SEGMENT_ID,
    value: nil,
    enabled: false,
    details: {}
  }

  begin
    # Step 1: Check if feature flag is enabled
    unless feature.enabled
      result_dict[:details][:value_type] = "DISABLED_VALUE"
      return EvaluationResult.new(
        value: feature.disabled_value,
        enabled: false,
        details: EvaluationDetails.new(**result_dict[:details])
      )
    end

    # Step 2: Check if feature has segment rules (targeting) and valid entity attributes
    if feature.segment_rules&.length&.positive? &&
       entity_attributes.is_a?(Hash) && entity_attributes.keys.length.positive?
      # Evaluate targeting rules
      rules_map = parse_rules(feature.segment_rules)
      result_dict = evaluate_rules(rules_map, entity_attributes, feature, nil, entity_id)
      return EvaluationResult.new(
        value: result_dict[:value],
        enabled: result_dict[:enabled],
        details: EvaluationDetails.new(**result_dict[:details])
      )
    end

    # Step 3: No targeting rules - apply default rollout percentage
    # Check if entity_id qualifies for rollout
    rollout_percentage, effective_entity_id = get_rollout_percentage(feature, nil, entity_id)
    normalized_value = get_normalized_value("#{effective_entity_id}:#{feature.feature_id}")

    if rollout_percentage == 100 ||
       normalized_value < rollout_percentage
      result_dict[:details][:value_type] = "ENABLED_VALUE"
      result_dict[:details][:rollout_percentage_applied] = true
      return EvaluationResult.new(
        value: feature.enabled_value,
        enabled: true,
        details: EvaluationDetails.new(**result_dict[:details])
      )
    end

    # Step 4: Entity doesn't qualify for rollout
    result_dict[:details][:value_type] = "DISABLED_VALUE"
    result_dict[:details][:rollout_percentage_applied] = false
    EvaluationResult.new(
      value: feature.disabled_value,
      enabled: false,
      details: EvaluationDetails.new(**result_dict[:details])
    )
  ensure
    # Always record evaluation for metering
    record_evaluation(feature.feature_id, nil, entity_id, result_dict[:evaluated_segment_id])
  end
end

#format_config(configurations, environment_id, collection_id) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 187

def format_config(configurations, environment_id, collection_id)
  {
    environments: [{
      environment_id: environment_id,
      features:       configurations[:features],
      properties:     configurations[:properties]
    }],
    collections: [{ collection_id: collection_id }],
    segments:     configurations[:segments]
  }
end

#get_feature(feature_id) ⇒ Feature?

Get feature by ID

Parameters:

  • feature_id (String)

    Feature ID

Returns:

  • (Feature, nil)

    Feature object or nil



382
383
384
385
386
387
388
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 382

def get_feature(feature_id)
  @cache_mutex.synchronize do
    return @feature_map[feature_id] if @feature_map.key?(feature_id)
  end
  @logger.error("Invalid feature id - #{feature_id}")
  nil
end

#get_featuresHash

Get features

Returns:

  • (Hash)

    Hash of features



748
749
750
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 748

def get_features
  @cache_mutex.synchronize { @feature_map.dup }
end

#get_propertiesHash

Get properties

Returns:

  • (Hash)

    Hash of properties



755
756
757
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 755

def get_properties
  @cache_mutex.synchronize { @property_map.dup }
end

#get_property(property_id) ⇒ Property?

Get property by ID

Parameters:

  • property_id (String)

    Property ID

Returns:

  • (Property, nil)

    Property object or nil



394
395
396
397
398
399
400
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 394

def get_property(property_id)
  @cache_mutex.synchronize do
    return @property_map[property_id] if @property_map.key?(property_id)
  end
  @logger.error("Invalid property id - #{property_id}")
  nil
end

#get_rollout_percentage(feature, segment_rule, entity_id) ⇒ Array(Integer, String)

Get rollout percentage for a feature or segment rule evaluation.

For progressive rollouts, the rollout configuration's start_at value is appended to the entity id so that the normalized hash used to decide eligibility is deterministic per rollout configuration (matches the Go SDK).

Parameters:

  • feature (Feature)

    Feature object

  • segment_rule (SegmentRules, nil)

    Segment rule object (nil for feature-level)

  • entity_id (String)

    Entity ID

Returns:

  • (Array(Integer, String))

    Tuple of [rollout percentage, effective entity id]



467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 467

def get_rollout_percentage(feature, segment_rule, entity_id)
  entity_id = entity_id.to_s

  if segment_rule
    # Segment-level rollout
    if segment_rule.rollout_configuration || segment_rule.rollout_type == Constants::PROGRESSIVE
      # Progressive rollout: inherit the feature-level configuration when the
      # segment rule uses the "$default" percentage, otherwise use the
      # segment-rule specific configuration.
      rollout_hash = @cache_mutex.synchronize do
        if segment_rule.rollout_percentage == Constants::DEFAULT_ROLLOUT_PERCENTAGE
          @rollout_config_map[feature.feature_id]
        else
          @rollout_config_map["#{feature.feature_id}#{Constants::DELIMITER}#{segment_rule.rule_id}"]
        end
      end

      return [0, entity_id] unless rollout_hash

      start_at = segment_rule.rollout_configuration && segment_rule.rollout_configuration[:start_at]
      entity_id += start_at.to_s if start_at
      [get_current_rollout_percentage(rollout_hash), entity_id]
    else
      # Manual rollout
      percentage = if segment_rule.rollout_percentage == Constants::DEFAULT_ROLLOUT_PERCENTAGE
                     feature.rollout_percentage
                   else
                     segment_rule.rollout_percentage
                   end
      [percentage, entity_id]
    end
  else
    # Feature-level rollout
    return [feature.rollout_percentage || 100, entity_id] unless feature.rollout_configuration

    rollout_hash = @cache_mutex.synchronize { @rollout_config_map[feature.feature_id] }
    return [0, entity_id] unless rollout_hash

    start_at = feature.rollout_configuration[:start_at]
    entity_id += start_at.to_s if start_at
    [get_current_rollout_percentage(rollout_hash), entity_id]
  end
end

#get_secret(property_id, secrets_manager_service) ⇒ SecretProperty?

Get secret property

Parameters:

  • property_id (String)

    Property ID

  • secrets_manager_service (Object)

    Secrets manager service

Returns:



407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 407

def get_secret(property_id, secrets_manager_service)
  property_obj = get_property(property_id)
  if property_obj
    if property_obj.type == Constants::SECRETREF
      @secret_map[property_id] = secrets_manager_service
      return SecretProperty.new(property_id)
    end
    @logger.error("Invalid operation: getSecret() cannot be called on a #{property_obj.type} property.")
    return nil
  end
  nil
end

#get_secrets_mapHash

Get the secrets map

Returns:

  • (Hash)

    Hash of secret manager instances mapped by property_id



821
822
823
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 821

def get_secrets_map
  @secret_map
end

#get_segment(segment_id) ⇒ Segment?

Get segment by ID

Parameters:

  • segment_id (String)

    Segment ID

Returns:

  • (Segment, nil)

    Segment object or nil



424
425
426
427
428
429
430
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 424

def get_segment(segment_id)
  @cache_mutex.synchronize do
    return @segment_map[segment_id] if @segment_map.key?(segment_id)
  end
  @logger.error("Invalid segment id - #{segment_id}")
  nil
end

#init(region:, guid:, apikey:, use_private_endpoint: false) ⇒ Object

Initialize the configuration handler

Parameters:

  • region (String)

    The region

  • guid (String)

    The GUID

  • apikey (String)

    The API key

  • use_private_endpoint (Boolean) (defaults to: false)

    Whether to use private endpoint



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 82

def init(region:, guid:, apikey:, use_private_endpoint: false)
  @guid = guid
  @region = region
  @apikey = apikey
  @use_private_endpoint = use_private_endpoint

  # Initialize UrlBuilder
  url_builder = UrlBuilder.instance
  url_builder.region = region
  url_builder.guid = guid
  url_builder.apikey = apikey
  url_builder.use_private_endpoint = use_private_endpoint

  # Initialize ApiManager
  ApiManager.set_authenticator

  # Initialize Metering
  metering_url = "#{url_builder.base_service_url}/apprapp/events/v1/instances/#{guid}/usage"
  Metering.instance.set_metering_url(metering_url, apikey)
end

#load_configurations_to_cache(data) ⇒ Object

Load configurations to cache

Parameters:

  • data (Hash)

    Configuration data



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 113

def load_configurations_to_cache(data)
  return unless data

  @cache_mutex.synchronize do
    if data[:features]
      features = data[:features]
      @all_feature_flags = features
      @feature_map = {}
      @rollout_config_map = {}

      features.each do |feature|
        feature_obj = Feature.new(feature)
        @feature_map[feature[:feature_id]] = feature_obj

        # Parse feature-level progressive rollout
        if feature_obj.rollout_configuration
          @rollout_config_map[feature[:feature_id]] =
            parse_rollout_configuration_phases(feature_obj.rollout_configuration)
        end

        # Parse segment-level progressive rollout
        next unless feature[:segment_rules].is_a?(Array)

        feature[:segment_rules].each do |segment_rule|
          segment_rule_obj = SegmentRules.new(segment_rule)
          if segment_rule_obj.rollout_configuration
            key = "#{feature[:feature_id]}#{Constants::DELIMITER}#{segment_rule[:rule_id]}"
            @rollout_config_map[key] = parse_rollout_configuration_phases(segment_rule_obj.rollout_configuration)
          end
        end
      end
    end

    if data[:properties]
      properties = data[:properties]
      @property_map = {}
      properties.each do |property|
        @property_map[property[:property_id]] = Property.new(property)
      end
    end

    if data[:segments]
      segments = data[:segments]
      @segment_map = {}
      segments.each do |segment|
        @segment_map[segment[:segment_id]] = Segment.new(segment)
      end
    end
  end

  # Notify listener after configurations are loaded (outside mutex to prevent
  # deadlock if the listener calls get_feature / get_property etc.)
  notify_configuration_update_listener
end

#log_error(error) ⇒ Object

Log error (non-raising — records the error in the log without interrupting flow)

Parameters:

  • error (String, Exception)

    Error message or exception



182
183
184
185
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 182

def log_error(error)
  error_msg = error.is_a?(Exception) ? error.message : error.to_s
  @logger.error(error_msg)
end

#notify_configuration_update_listenerObject

Notify the registered configuration update listener This method is called internally when configurations are updated. The listener is invoked safely - exceptions are caught to prevent breaking the update flow.



807
808
809
810
811
812
813
814
815
816
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 807

def notify_configuration_update_listener
  return unless @configuration_update_listener

  begin
    @logger.log(Constants::CONFIG_UPDATE_NOTIFYING_LISTENER)
    @configuration_update_listener.call
  rescue StandardError => e
    @logger.error("Error in configuration update listener: #{e.class.name} - #{e.message}")
  end
end

#parse_rules(segment_rules) ⇒ Hash

Parse rules

Parameters:

  • segment_rules (Array)

    Segment rules

Returns:

  • (Hash)

    Parsed rules



448
449
450
451
452
453
454
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 448

def parse_rules(segment_rules)
  rules_map = {}
  segment_rules.each do |rules|
    rules_map[rules[:order]] = SegmentRules.new(rules)
  end
  rules_map
end

#property_evaluation(property, entity_id, entity_attributes) ⇒ EvaluationResult

Property evaluation

Parameters:

  • property (Property)

    Property object

  • entity_id (String)

    Entity ID

  • entity_attributes (Hash)

    Entity attributes

Returns:

  • (EvaluationResult)

    Evaluation result with value and details (+enabled+ is nil)



712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 712

def property_evaluation(property, entity_id, entity_attributes)
  result_dict = {
    evaluated_segment_id: Constants::DEFAULT_SEGMENT_ID,
    value: nil,
    details: {}
  }

  begin
    # Check whether the property is configured with any targeting definition
    # and then check whether the user has passed valid entityAttributes JSON before we evaluate
    if property.segment_rules&.length&.positive? &&
       entity_attributes.is_a?(Hash) && entity_attributes.keys.length.positive?
      rules_map = parse_rules(property.segment_rules)
      result_dict = evaluate_rules(rules_map, entity_attributes, nil, property, entity_id)
      return EvaluationResult.new(
        value: result_dict[:value],
        enabled: nil,
        details: EvaluationDetails.new(**result_dict[:details])
      )
    end

    result_dict[:details][:value_type] = "DEFAULT_VALUE"
    EvaluationResult.new(
      value: property.value,
      enabled: nil,
      details: EvaluationDetails.new(**result_dict[:details])
    )
  ensure
    # Record evaluation for metering
    record_evaluation(nil, property.property_id, entity_id, result_dict[:evaluated_segment_id])
  end
end

#record_evaluation(feature_id, property_id, entity_id, segment_id) ⇒ Object

Record evaluation

Parameters:

  • feature_id (String)

    Feature ID

  • property_id (String)

    Property ID

  • entity_id (String)

    Entity ID

  • segment_id (String)

    Segment ID



626
627
628
629
630
631
632
633
634
635
636
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 626

def record_evaluation(feature_id, property_id, entity_id, segment_id)
  Metering.instance.add_metering(
    @guid,
    @environment_id,
    @collection_id,
    entity_id || Constants::DEFAULT_ENTITY_ID,
    segment_id || Constants::DEFAULT_SEGMENT_ID,
    feature_id,
    property_id
  )
end

#register_configuration_update_listener(&block) ⇒ Object

Register configuration update listener Registers a callback block that will be invoked when configurations are updated. Only one listener can be registered at a time (matches Java SDK behavior). Calling this method multiple times will replace the previous listener.

Parameters:

  • block (Proc)

    Callback block to be invoked on configuration updates



793
794
795
796
797
798
799
800
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 793

def register_configuration_update_listener(&block)
  if block_given?
    @configuration_update_listener = block
    @logger.log(Constants::CONFIG_UPDATE_LISTENER_REGISTERED)
  else
    @logger.warning(Constants::CONFIG_UPDATE_LISTENER_NO_BLOCK)
  end
end

#set_bootstrap_file(bootstrap_file) ⇒ Object

Set bootstrap file

Parameters:

  • bootstrap_file (String)

    Path to bootstrap file



776
777
778
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 776

def set_bootstrap_file(bootstrap_file)
  @bootstrap_file = bootstrap_file
end

#set_context(collection_id, environment_id, **options) ⇒ Object

Set context for configuration

Parameters:

  • collection_id (String)

    Collection ID

  • environment_id (String)

    Environment ID

  • options (Hash)

    Additional options



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 204

def set_context(collection_id, environment_id, **options)
  @collection_id = collection_id
  @environment_id = environment_id
  @persistent_cache_directory = options[:persistent_cache_directory]
  @bootstrap_file = options[:bootstrap_file]
  @live_update = options[:live_config_update_enabled]

  # EvaluationEvents and MetricEvents are not yet ported from Node SDK.

  persistent_cache_read = false
  error_reading_bootstrap_config = false

  # Handle persistent cache directory
  if @persistent_cache_directory
    @logger.info("persistent cache directory path is: #{@persistent_cache_directory}")
    file_path = File.join(@persistent_cache_directory, "appconfiguration.json")
    persistent_cache = @file_manager.read_persistent_cache_configurations(file_path)

    unless persistent_cache.empty?
      configurations = extract_configurations(JSON.parse(persistent_cache), @environment_id, @collection_id)
      load_configurations_to_cache(configurations)
      persistent_cache_read = true
    end

    # Check write permissions
    begin
      # Test if directory is writable
      File.write(File.join(@persistent_cache_directory, ".write_test"), "")
      File.delete(File.join(@persistent_cache_directory, ".write_test"))
    rescue StandardError => e
      log_error("ERROR: No write permission for persistent cache directory. #{e}")
    end
  end

  # Handle bootstrap file
  if @bootstrap_file
    if @persistent_cache_directory
      # If persistent cache directory exists
      if persistent_cache_read
        # Listener already fired inside load_configurations_to_cache above.
      else
        # Only read bootstrap if persistent cache wasn't read
        begin
          @logger.info("reading configurations from bootstrap file: #{@bootstrap_file}")
          bootstrap_config = @file_manager.read_bootstrap_configurations_from_file(@bootstrap_file)
          configurations = extract_configurations(JSON.parse(bootstrap_config), @environment_id, @collection_id)
          load_configurations_to_cache(configurations)
          write_to_persistent_storage(format_config(configurations, @environment_id, @collection_id))
          # Listener already fired inside load_configurations_to_cache above.
        rescue StandardError => e
          log_error(e)
        end
      end
    else
      # No persistent cache directory, just read bootstrap
      @logger.info("reading configurations from bootstrap file: #{@bootstrap_file}")
      begin
        bootstrap_config = @file_manager.read_bootstrap_configurations_from_file(@bootstrap_file)
        configurations = extract_configurations(JSON.parse(bootstrap_config, symbolize_names: true),
                                                @environment_id, @collection_id)
        load_configurations_to_cache(configurations)
        # Listener already fired inside load_configurations_to_cache above.
      rescue StandardError => e
        log_error(e) unless @live_update
        @logger.error(e.message.to_s)
        error_reading_bootstrap_config = true
      end
    end
  end

  # Implement live update logic
  return unless @live_update

  @logger.info(Constants::CONFIG_LIVE_UPDATE_ENABLED)

  # Track whether to start background retry
  start_background_retry = false

  # Create config fetcher instance
  config_fetcher = ConfigFetcher.new(
    collection_id: @collection_id,
    environment_id: @environment_id,
    handler: self,
    logger: @logger
  )

  # Fetch configurations from API
  fetch_result = config_fetcher.fetch

  if fetch_result[:ok]
    @logger.log(Constants::SUCCESSFULLY_FETCHED_THE_CONFIGURATIONS)

    # Process and load configurations
    begin
      # Parse JSON string if the IBM SDK returned the body unparsed, then symbolize keys
      raw = fetch_result[:data]
      raw = JSON.parse(raw) if raw.is_a?(String)
      symbolized_data = symbolize_keys(raw)

      # Extract configurations using utils.rb method
      extracted_config = extract_configurations(
        symbolized_data,
        @environment_id,
        @collection_id
      )

      # Load to cache using existing method
      load_configurations_to_cache(extracted_config)

      # Write to persistent storage if configured
      if @persistent_cache_directory
        formatted_config = format_config(extracted_config, @environment_id, @collection_id)
        write_to_persistent_storage(formatted_config)
      end

      @logger.log(Constants::CONFIG_LOADED_SUCCESSFULLY)
    rescue StandardError => e
      @logger.error("Failed to process configurations: #{e.class.name} - #{e.message}")
    end
  else
    # Failed to fetch from API
    status_code = fetch_result[:status]
    err_msg = "Status code: #{status_code}. Message: Failed to fetch the configurations from remote server."

    # Check for client-side errors (4xx except 429)
    log_error(err_msg) if status_code >= 400 && status_code < 500 && status_code != 429

    # Check if we have fallback configurations (persistent cache or bootstrap)
    if persistent_cache_read
      message = "Loaded the configurations from the persistent cache into the application."
      @logger.info("#{err_msg} #{message}")
      start_background_retry = true
      notify_configuration_update_listener
    elsif @bootstrap_file && !error_reading_bootstrap_config
      message = "Loaded the configurations from the bootstrap file: #{@bootstrap_file} into the application."
      @logger.info("#{err_msg} #{message}")
      start_background_retry = true
      notify_configuration_update_listener
    else
      # No fallback available
      @logger.error(Constants::CONFIG_NO_CONFIGURATIONS_AVAILABLE)
      log_error(err_msg)
    end
  end

  # Start WebSocket client for live updates
  @logger.log(Constants::CONFIG_STARTING_WEBSOCKET)
  begin
    # Get required parameters from UrlBuilder
    url_builder = UrlBuilder.instance

    # Set @guid from url_builder if not already set
    @guid ||= url_builder.guid

    @websocket_client = WebSocketClient.new(
      region: url_builder.region,
      guid: @guid,
      apikey: url_builder.apikey,
      collection_id: @collection_id,
      environment_id: @environment_id,
      start_background_retry: start_background_retry
    )

    @websocket_client.connect
    @logger.log(Constants::CONFIG_WEBSOCKET_STARTED)
  rescue StandardError => e
    @logger.error("Failed to start WebSocket client: #{e.class.name} - #{e.message}")
  end
end

#set_live_update(live_update) ⇒ Object

Set live update status

Parameters:

  • live_update (Boolean)

    Live update status



769
770
771
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 769

def set_live_update(live_update)
  @live_update = live_update
end

#set_persistent_cache_directory(directory) ⇒ Object

Set persistent cache directory

Parameters:

  • directory (String)

    Cache directory path



783
784
785
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 783

def set_persistent_cache_directory(directory)
  @persistent_cache_directory = directory
end

#track(_event_key, _entity_id) ⇒ Object

Raises:

  • (NotImplementedError)


374
375
376
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 374

def track(_event_key, _entity_id)
  raise NotImplementedError, "track is not yet implemented — requires MetricEvents port from Node SDK"
end

#write_to_persistent_storage(file_data) ⇒ Object

Write to persistent storage

Parameters:

  • file_data (Hash)

    File data to persist



171
172
173
174
175
176
177
# File 'lib/ibm_appconfiguration_ruby_sdk/configuration_handler.rb', line 171

def write_to_persistent_storage(file_data)
  return unless @persistent_cache_directory

  json = JSON.generate(file_data)
  file_path = File.join(@persistent_cache_directory, "appconfiguration.json")
  @file_manager.store_files(json, file_path)
end