rox-rollout 6.0.0 → 6.0.2
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 +4 -4
- data/.gitignore +10 -0
- data/Jenkinsfile +212 -0
- data/README.md +1 -1
- data/cbci-templates/sdkci-e2e.yml +29 -0
- data/cbci-templates/sdkci-unit.yml +67 -0
- data/e2e-server/run_server.sh +2 -1
- data/examples/simple_example.rb +78 -0
- data/lib/rox/core/core.rb +2 -1
- data/lib/rox/core/notifications/notification_listener.rb +149 -18
- data/lib/rox/version.rb +1 -1
- data/rox.gemspec +7 -7
- metadata +22 -20
- data/.github/workflows/e2e_tests.yaml +0 -52
- data/.github/workflows/unit_tests.yaml +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bffb064d1154d2a9a765e1cd7592cabaf2433c385945694f7373a62ddf6b052f
|
|
4
|
+
data.tar.gz: 682bc11092f95c2a1ac04a4daac434316f8cbf93ee897f60738117d2d5a932a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 772253981c9790addeb0995d564053493c70db175a8404c9e7200a2521206f801e11c1c44327b6cd2d03cf4db4d7e84007d10139ff57893b17a8bbcf50ce621b
|
|
7
|
+
data.tar.gz: b9941e8442d8d66bd91373d5f076a24a92da617798af776d46c99e11f000d338fef6186bcbc642107af80e59d88c47e094053bf191912e9a06ff6938c0220f16
|
data/.gitignore
CHANGED
data/Jenkinsfile
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
pipeline {
|
|
2
|
+
agent any
|
|
3
|
+
|
|
4
|
+
libraries {
|
|
5
|
+
lib('fm-shared-library@main')
|
|
6
|
+
}//end libraries. Github Repo: https://github.com/rollout/fm-cbci-shared-library
|
|
7
|
+
|
|
8
|
+
options {
|
|
9
|
+
timeout(time: 45, unit: 'MINUTES')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
stages {
|
|
13
|
+
stage('Checkout') {
|
|
14
|
+
steps {
|
|
15
|
+
checkout scm
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
stage("Run Unit tests"){
|
|
20
|
+
parallel {
|
|
21
|
+
stage("ruby-2.6") {
|
|
22
|
+
agent {
|
|
23
|
+
kubernetes {
|
|
24
|
+
inheritFrom 'ruby'
|
|
25
|
+
yamlFile './cbci-templates/sdkci-unit.yml'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
steps {
|
|
30
|
+
container(name: "ruby-2-6", shell: "sh") {
|
|
31
|
+
withCredentials([
|
|
32
|
+
sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
|
|
33
|
+
]) {
|
|
34
|
+
echo "Executing Run tests"
|
|
35
|
+
sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
|
|
36
|
+
label: "Running unit tests"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
stage("ruby-2.7") {
|
|
42
|
+
agent {
|
|
43
|
+
kubernetes {
|
|
44
|
+
inheritFrom 'ruby'
|
|
45
|
+
yamlFile './cbci-templates/sdkci-unit.yml'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
steps {
|
|
50
|
+
container(name: "ruby-2-7", shell: "sh") {
|
|
51
|
+
withCredentials([
|
|
52
|
+
sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
|
|
53
|
+
]) {
|
|
54
|
+
echo "Executing Run tests"
|
|
55
|
+
sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
|
|
56
|
+
label: "Running unit tests"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
stage("ruby-3.0") {
|
|
62
|
+
agent {
|
|
63
|
+
kubernetes {
|
|
64
|
+
inheritFrom 'ruby'
|
|
65
|
+
yamlFile './cbci-templates/sdkci-unit.yml'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
steps {
|
|
70
|
+
container(name: "ruby-3-0", shell: "sh") {
|
|
71
|
+
withCredentials([
|
|
72
|
+
sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
|
|
73
|
+
]) {
|
|
74
|
+
echo "Executing Run tests"
|
|
75
|
+
sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
|
|
76
|
+
label: "Running unit tests"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
stage("ruby-3.1") {
|
|
82
|
+
agent {
|
|
83
|
+
kubernetes {
|
|
84
|
+
inheritFrom 'ruby'
|
|
85
|
+
yamlFile './cbci-templates/sdkci-unit.yml'
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
steps {
|
|
90
|
+
container(name: "ruby-3-1", shell: "sh") {
|
|
91
|
+
withCredentials([
|
|
92
|
+
sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
|
|
93
|
+
]) {
|
|
94
|
+
echo "Executing Run tests"
|
|
95
|
+
sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
|
|
96
|
+
label: "Running unit tests"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
stage("ruby-3.2") {
|
|
102
|
+
agent {
|
|
103
|
+
kubernetes {
|
|
104
|
+
inheritFrom 'ruby'
|
|
105
|
+
yamlFile './cbci-templates/sdkci-unit.yml'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
steps {
|
|
110
|
+
container(name: "ruby-3-2", shell: "sh") {
|
|
111
|
+
withCredentials([
|
|
112
|
+
sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
|
|
113
|
+
]) {
|
|
114
|
+
echo "Executing Run tests"
|
|
115
|
+
sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
|
|
116
|
+
label: "Running unit tests"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
stage("ruby-3.3") {
|
|
122
|
+
agent {
|
|
123
|
+
kubernetes {
|
|
124
|
+
inheritFrom 'ruby'
|
|
125
|
+
yamlFile './cbci-templates/sdkci-unit.yml'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
steps {
|
|
130
|
+
container(name: "ruby-3-3", shell: "sh") {
|
|
131
|
+
withCredentials([
|
|
132
|
+
sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
|
|
133
|
+
]) {
|
|
134
|
+
echo "Executing Run tests"
|
|
135
|
+
sh script: 'gem install bundler -v 2.4.22 && bundle install && bundle exec rake test',
|
|
136
|
+
label: "Running unit tests"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
post{
|
|
143
|
+
success{
|
|
144
|
+
script {
|
|
145
|
+
echo 'Unit Tests OK; posting results'
|
|
146
|
+
currentBuild.result = 'SUCCESS'
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
failure{
|
|
150
|
+
echo 'Unit Tests Failed;'
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
stage("Run E2E tests"){
|
|
155
|
+
agent {
|
|
156
|
+
kubernetes {
|
|
157
|
+
inheritFrom 'default'
|
|
158
|
+
yamlFile './cbci-templates/sdkci-e2e.yml'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
steps {
|
|
163
|
+
container("rox-proxy") {
|
|
164
|
+
waitForRoxProxy()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
container(name: "server", shell: 'sh') {
|
|
168
|
+
withCredentials([
|
|
169
|
+
string(credentialsId: 'TEST_E2E_BEARER', variable: 'TEST_E2E_BEARER'),
|
|
170
|
+
sshUserPrivateKey(credentialsId: 'SDK_E2E_SSH_KEY', keyFileVariable: 'SDK_E2E_SSH_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
|
|
171
|
+
sshUserPrivateKey(credentialsId: 'SDK_E2E_TESTS_DEPLOY_KEY', keyFileVariable: 'SDK_E2E_TESTS_DEPLOY_KEY', passphraseVariable: '', usernameVariable: 'cloudbees.eslint@cloudbees.com'),
|
|
172
|
+
]) {
|
|
173
|
+
script {
|
|
174
|
+
addGitHubFingerprint()
|
|
175
|
+
TESTENVPARAMS = "QA_E2E_BEARER=$TEST_E2E_BEARER API_HOST=https://api.test.rollout.io CD_API_ENDPOINT=https://api.test.rollout.io/device/get_configuration CD_S3_ENDPOINT=https://rox-conf.test.rollout.io/ SS_API_ENDPOINT=https://api.test.rollout.io/device/update_state_store/ SS_S3_ENDPOINT=https://rox-state.test.rollout.io/ CLIENT_DATA_CACHE_KEY=client_data ANALYTICS_ENDPOINT=https://analytic.test.rollout.io/ NOTIFICATIONS_ENDPOINT=https://push.test.rollout.io/sse"
|
|
176
|
+
ROOT_DIR = pwd()
|
|
177
|
+
|
|
178
|
+
withEnv(["GIT_SSH_COMMAND=ssh -i ${SDK_E2E_SSH_KEY}"]) {
|
|
179
|
+
echo "Executing E2E tests"
|
|
180
|
+
sh script: """
|
|
181
|
+
apt-get update && apt-get install -y curl gnupg
|
|
182
|
+
curl -sL https://deb.nodesource.com/setup_lts.x | bash -
|
|
183
|
+
apt-get install -y nodejs && npm install -g yarn
|
|
184
|
+
|
|
185
|
+
git clone git@github.com:rollout/sdk-end-2-end-tests.git
|
|
186
|
+
ln -s ${ROOT_DIR}/e2e-server/ ${ROOT_DIR}/sdk-end-2-end-tests/drivers/ruby
|
|
187
|
+
|
|
188
|
+
cd ${ROOT_DIR}/sdk-end-2-end-tests/drivers/nodejs && yarn install --frozen-lockfile
|
|
189
|
+
cd ${ROOT_DIR}/sdk-end-2-end-tests && yarn install --frozen-lockfile
|
|
190
|
+
|
|
191
|
+
${TESTENVPARAMS} SDK_LANG=ruby NODE_ENV=container yarn test:env
|
|
192
|
+
""", label: "Pull SDK end2 tests repository"
|
|
193
|
+
}// end withEnv
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
post{
|
|
199
|
+
success{
|
|
200
|
+
script {
|
|
201
|
+
echo 'E2E Tests OK; posting results'
|
|
202
|
+
currentBuild.result = 'SUCCESS'
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
failure{
|
|
206
|
+
echo 'E2E Tests Failed;'
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
data/README.md
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: Pod
|
|
3
|
+
metadata:
|
|
4
|
+
name: rox-ruby-sdk-e2e
|
|
5
|
+
spec:
|
|
6
|
+
serviceAccountName: ops-gcr-rw
|
|
7
|
+
containers:
|
|
8
|
+
- name: server
|
|
9
|
+
image: ruby:3.2
|
|
10
|
+
tty: true
|
|
11
|
+
resources:
|
|
12
|
+
requests:
|
|
13
|
+
memory: "1Gi"
|
|
14
|
+
cpu: "1000m"
|
|
15
|
+
limits:
|
|
16
|
+
memory: "2Gi"
|
|
17
|
+
cpu: "1000m"
|
|
18
|
+
- name: rox-proxy
|
|
19
|
+
image: rollout/simple-proxy
|
|
20
|
+
tty: true
|
|
21
|
+
ports:
|
|
22
|
+
- containerPort: 8080
|
|
23
|
+
resources:
|
|
24
|
+
requests:
|
|
25
|
+
memory: "1Gi"
|
|
26
|
+
cpu: "1000m"
|
|
27
|
+
limits:
|
|
28
|
+
memory: "1Gi"
|
|
29
|
+
cpu: "1000m"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
apiVersion: v1
|
|
2
|
+
kind: Pod
|
|
3
|
+
metadata:
|
|
4
|
+
name: rox-ruby-sdk-unit
|
|
5
|
+
spec:
|
|
6
|
+
serviceAccountName: ops-gcr-rw
|
|
7
|
+
containers:
|
|
8
|
+
- name: ruby-2-6
|
|
9
|
+
image: ruby:2.6
|
|
10
|
+
tty: true
|
|
11
|
+
resources:
|
|
12
|
+
requests:
|
|
13
|
+
memory: "1Gi"
|
|
14
|
+
cpu: "1000m"
|
|
15
|
+
limits:
|
|
16
|
+
memory: "2Gi"
|
|
17
|
+
cpu: "1000m"
|
|
18
|
+
- name: ruby-2-7
|
|
19
|
+
image: ruby:2.7
|
|
20
|
+
tty: true
|
|
21
|
+
resources:
|
|
22
|
+
requests:
|
|
23
|
+
memory: "1Gi"
|
|
24
|
+
cpu: "1000m"
|
|
25
|
+
limits:
|
|
26
|
+
memory: "2Gi"
|
|
27
|
+
cpu: "1000m"
|
|
28
|
+
- name: ruby-3-0
|
|
29
|
+
image: ruby:3.0
|
|
30
|
+
tty: true
|
|
31
|
+
resources:
|
|
32
|
+
requests:
|
|
33
|
+
memory: "1Gi"
|
|
34
|
+
cpu: "1000m"
|
|
35
|
+
limits:
|
|
36
|
+
memory: "2Gi"
|
|
37
|
+
cpu: "1000m"
|
|
38
|
+
- name: ruby-3-1
|
|
39
|
+
image: ruby:3.1
|
|
40
|
+
tty: true
|
|
41
|
+
resources:
|
|
42
|
+
requests:
|
|
43
|
+
memory: "1Gi"
|
|
44
|
+
cpu: "1000m"
|
|
45
|
+
limits:
|
|
46
|
+
memory: "2Gi"
|
|
47
|
+
cpu: "1000m"
|
|
48
|
+
- name: ruby-3-2
|
|
49
|
+
image: ruby:3.2
|
|
50
|
+
tty: true
|
|
51
|
+
resources:
|
|
52
|
+
requests:
|
|
53
|
+
memory: "1Gi"
|
|
54
|
+
cpu: "1000m"
|
|
55
|
+
limits:
|
|
56
|
+
memory: "2Gi"
|
|
57
|
+
cpu: "1000m"
|
|
58
|
+
- name: ruby-3-3
|
|
59
|
+
image: ruby:3.3
|
|
60
|
+
tty: true
|
|
61
|
+
resources:
|
|
62
|
+
requests:
|
|
63
|
+
memory: "1Gi"
|
|
64
|
+
cpu: "1000m"
|
|
65
|
+
limits:
|
|
66
|
+
memory: "2Gi"
|
|
67
|
+
cpu: "1000m"
|
data/e2e-server/run_server.sh
CHANGED
|
@@ -7,8 +7,9 @@ DIR=`pwd`
|
|
|
7
7
|
gem install sinatra --conservative
|
|
8
8
|
gem install sinatra-contrib --conservative
|
|
9
9
|
gem install json --conservative
|
|
10
|
-
gem install
|
|
10
|
+
gem install server_sent_events --conservative
|
|
11
11
|
gem install rackup
|
|
12
|
+
gem install webrick
|
|
12
13
|
|
|
13
14
|
nohup ruby ./server.rb $1 1>"$DIR"/log_"$1".out 2>&1 &
|
|
14
15
|
while true ; do
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Simple example to test Rox SDK with Ruby 3.3
|
|
5
|
+
require 'bundler/setup'
|
|
6
|
+
require 'rox/server/rox_server'
|
|
7
|
+
require 'rox/server/flags/rox_flag'
|
|
8
|
+
require 'rox/server/flags/rox_string'
|
|
9
|
+
|
|
10
|
+
# Define a container class for flags
|
|
11
|
+
class FeatureFlags
|
|
12
|
+
attr_reader :enable_tutorial, :color_variant, :timeout_value
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@enable_tutorial = Rox::Server::RoxFlag.new(false)
|
|
16
|
+
@color_variant = Rox::Server::RoxString.new('blue', ['red', 'green', 'blue'])
|
|
17
|
+
@timeout_value = Rox::Server::RoxInt.new(30, [10, 30, 60])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Initialize flags
|
|
22
|
+
flags = FeatureFlags.new
|
|
23
|
+
|
|
24
|
+
# Register flags
|
|
25
|
+
puts "Registering feature flags..."
|
|
26
|
+
Rox::Server::RoxServer.register(flags)
|
|
27
|
+
|
|
28
|
+
# Set custom properties
|
|
29
|
+
puts "Setting custom properties..."
|
|
30
|
+
Rox::Server::RoxServer.set_custom_string_property('tier', 'premium')
|
|
31
|
+
Rox::Server::RoxServer.set_custom_boolean_property('is_beta') { true }
|
|
32
|
+
|
|
33
|
+
# Setup SDK with test API key
|
|
34
|
+
puts "Setting up Rox SDK..."
|
|
35
|
+
puts "Ruby version: #{RUBY_VERSION}"
|
|
36
|
+
puts "Rox SDK version: #{Rox::VERSION}"
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
# Use a test API key in valid format (24-char hex for MongoDB ObjectId)
|
|
40
|
+
# This will fail to connect but will test initialization
|
|
41
|
+
api_key = '507f1f77bcf86cd799439011'
|
|
42
|
+
|
|
43
|
+
options = Rox::Server::RoxOptions.new(
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
fetch_interval: 60
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
Rox::Server::RoxServer.setup(api_key, options)
|
|
49
|
+
|
|
50
|
+
# Give it a moment to initialize
|
|
51
|
+
sleep 2
|
|
52
|
+
|
|
53
|
+
# Test flag evaluation (will use defaults since we can't fetch)
|
|
54
|
+
puts "\nTesting flag evaluation:"
|
|
55
|
+
puts "enable_tutorial: #{flags.enable_tutorial.enabled?(nil)}"
|
|
56
|
+
puts "color_variant: #{flags.color_variant.value(nil)}"
|
|
57
|
+
puts "timeout_value: #{flags.timeout_value.value(nil)}"
|
|
58
|
+
|
|
59
|
+
# Test dynamic API
|
|
60
|
+
puts "\nTesting dynamic API:"
|
|
61
|
+
dynamic_api = Rox::Server::RoxServer.dynamic_api
|
|
62
|
+
puts "Dynamic flag (default false): #{dynamic_api.enabled?('dynamic.test', false, nil)}"
|
|
63
|
+
puts "Dynamic value (default 'test'): #{dynamic_api.value('dynamic.string', 'test', nil)}"
|
|
64
|
+
|
|
65
|
+
puts "\n✅ Ruby #{RUBY_VERSION} compatibility test PASSED!"
|
|
66
|
+
puts "SDK initialized and flags evaluated successfully."
|
|
67
|
+
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
puts "\n❌ Error: #{e.message}"
|
|
70
|
+
puts e.backtrace.first(5)
|
|
71
|
+
exit 1
|
|
72
|
+
ensure
|
|
73
|
+
# Shutdown SDK
|
|
74
|
+
puts "\nShutting down SDK..."
|
|
75
|
+
Rox::Server::RoxServer.shutdown
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
puts "\nSample app completed successfully!"
|
data/lib/rox/core/core.rb
CHANGED
|
@@ -201,7 +201,8 @@ module Rox
|
|
|
201
201
|
def start_or_stop_push_updated_listener
|
|
202
202
|
if @internal_flags.enabled?('rox.internal.pushUpdates')
|
|
203
203
|
if @push_updates_listener.nil?
|
|
204
|
-
|
|
204
|
+
logger = @rox_options&.logger
|
|
205
|
+
@push_updates_listener = NotificationListener.new(Environment.notifications_path, @sdk_settings.api_key, logger: logger)
|
|
205
206
|
@push_updates_listener.on 'changed' do |_data|
|
|
206
207
|
fetch
|
|
207
208
|
end
|
|
@@ -1,12 +1,61 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'server_sent_events'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'uri'
|
|
2
4
|
|
|
3
5
|
module Rox
|
|
4
6
|
module Core
|
|
5
7
|
class NotificationListener
|
|
6
|
-
|
|
8
|
+
# Initial reconnection delay in seconds (SSE spec recommends "a few seconds")
|
|
9
|
+
INITIAL_RECONNECT_DELAY = 3
|
|
10
|
+
# Maximum reconnection delay in seconds
|
|
11
|
+
MAX_RECONNECT_DELAY = 60
|
|
12
|
+
# Reconnection backoff multiplier
|
|
13
|
+
RECONNECT_MULTIPLIER = 2
|
|
14
|
+
# Jitter percentage to prevent thundering herd (±20%)
|
|
15
|
+
JITTER_FACTOR = 0.2
|
|
16
|
+
|
|
17
|
+
# Extended SSE Client that adds proper headers and timeout configuration
|
|
18
|
+
# The base ServerSentEvents::Client lacks these features
|
|
19
|
+
class SSEClient < ServerSentEvents::Client
|
|
20
|
+
def initialize(address, parser, open_timeout: 10, read_timeout: nil)
|
|
21
|
+
super(address, parser)
|
|
22
|
+
@open_timeout = open_timeout
|
|
23
|
+
@read_timeout = read_timeout
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def listen(&block)
|
|
27
|
+
Net::HTTP.start(
|
|
28
|
+
@address.host,
|
|
29
|
+
@address.port,
|
|
30
|
+
use_ssl: @address.scheme == 'https',
|
|
31
|
+
open_timeout: @open_timeout,
|
|
32
|
+
read_timeout: @read_timeout
|
|
33
|
+
) do |http|
|
|
34
|
+
request = Net::HTTP::Get.new(@address)
|
|
35
|
+
request['Accept'] = 'text/event-stream'
|
|
36
|
+
request['Cache-Control'] = 'no-cache'
|
|
37
|
+
|
|
38
|
+
http.request(request) do |response|
|
|
39
|
+
unless response.code == '200'
|
|
40
|
+
raise "HTTP #{response.code}: #{response.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
response.read_body do |chunk|
|
|
44
|
+
@parser.push(chunk).each { |event| block.call(event) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(listen_url, app_key, logger: nil)
|
|
7
52
|
@listen_url = listen_url
|
|
8
53
|
@app_key = app_key
|
|
9
54
|
@handlers = {}
|
|
55
|
+
@logger = logger
|
|
56
|
+
@thread = nil
|
|
57
|
+
@running = false
|
|
58
|
+
@reconnect_delay = INITIAL_RECONNECT_DELAY
|
|
10
59
|
end
|
|
11
60
|
|
|
12
61
|
def on(event_name, &handler)
|
|
@@ -15,31 +64,113 @@ module Rox
|
|
|
15
64
|
end
|
|
16
65
|
|
|
17
66
|
def start
|
|
67
|
+
return if @running
|
|
68
|
+
|
|
69
|
+
@running = true
|
|
18
70
|
sse_url = "#{@listen_url.chomp('/')}/#{@app_key}"
|
|
71
|
+
|
|
19
72
|
@thread = Thread.new do
|
|
20
|
-
|
|
21
|
-
source = EventMachine::EventSource.new(sse_url)
|
|
22
|
-
@handlers.each do |event_name, event_handlers|
|
|
23
|
-
event_handlers.each do |handler|
|
|
24
|
-
source.on event_name do |data|
|
|
25
|
-
# Start new thread to allow the handler to stop the Listener (terminate the current thread)
|
|
26
|
-
# and continue handler code execution without interruption
|
|
27
|
-
handler_thread = Thread.new do
|
|
28
|
-
handler.call(data)
|
|
29
|
-
end
|
|
30
|
-
handler_thread.join
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
source.start
|
|
35
|
-
end
|
|
73
|
+
connect_with_retry(sse_url)
|
|
36
74
|
end
|
|
37
75
|
end
|
|
38
76
|
|
|
39
77
|
def stop
|
|
78
|
+
@running = false
|
|
40
79
|
@thread&.terminate
|
|
41
80
|
@thread = nil
|
|
42
81
|
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def calculate_reconnect_delay
|
|
86
|
+
# Use current delay, capped at maximum
|
|
87
|
+
base_delay = [@reconnect_delay, MAX_RECONNECT_DELAY].min
|
|
88
|
+
|
|
89
|
+
# Add jitter: ±20% randomness to prevent thundering herd
|
|
90
|
+
# Example: 3s becomes 2.4s - 3.6s
|
|
91
|
+
# This spreads out reconnection attempts across clients
|
|
92
|
+
jitter = rand(-JITTER_FACTOR..JITTER_FACTOR) * base_delay
|
|
93
|
+
final_delay = base_delay + jitter
|
|
94
|
+
|
|
95
|
+
# Never less than 1 second
|
|
96
|
+
[final_delay, 1.0].max
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def connect_with_retry(sse_url)
|
|
100
|
+
while @running
|
|
101
|
+
begin
|
|
102
|
+
log_info("Connecting to SSE endpoint: #{sse_url}")
|
|
103
|
+
connect_to_sse(sse_url)
|
|
104
|
+
|
|
105
|
+
# Connection closed normally - reset to initial delay
|
|
106
|
+
break unless @running
|
|
107
|
+
@reconnect_delay = INITIAL_RECONNECT_DELAY
|
|
108
|
+
|
|
109
|
+
rescue => e
|
|
110
|
+
break unless @running
|
|
111
|
+
log_error("SSE connection error: #{e.class} - #{e.message}")
|
|
112
|
+
|
|
113
|
+
# On error, use exponential backoff for next attempt
|
|
114
|
+
@reconnect_delay = [@reconnect_delay * RECONNECT_MULTIPLIER, MAX_RECONNECT_DELAY].min
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# ALWAYS delay before reconnecting (both normal close and error cases)
|
|
118
|
+
# This matches em-eventsource behavior which waits ~3s regardless of close reason
|
|
119
|
+
delay = calculate_reconnect_delay
|
|
120
|
+
log_info("Reconnecting in #{delay.round(1)} seconds...")
|
|
121
|
+
sleep(delay)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def connect_to_sse(sse_url)
|
|
126
|
+
uri = URI(sse_url)
|
|
127
|
+
|
|
128
|
+
# Create SSE client using the library's client with our extensions
|
|
129
|
+
client = SSEClient.new(
|
|
130
|
+
uri,
|
|
131
|
+
ServerSentEvents::Parser.new,
|
|
132
|
+
open_timeout: 10, # 10 seconds to establish connection
|
|
133
|
+
read_timeout: nil # No timeout for reading (SSE is long-lived)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
log_info("Connected to SSE endpoint")
|
|
137
|
+
@reconnect_delay = INITIAL_RECONNECT_DELAY
|
|
138
|
+
|
|
139
|
+
# Use the library's listen method to handle connection and parsing
|
|
140
|
+
client.listen do |event|
|
|
141
|
+
break unless @running
|
|
142
|
+
handle_event(event)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def handle_event(event)
|
|
147
|
+
# Get event type (default to 'message' if not specified)
|
|
148
|
+
event_type = event.event || 'message'
|
|
149
|
+
|
|
150
|
+
# Call registered handlers for this event type
|
|
151
|
+
handlers = @handlers[event_type]
|
|
152
|
+
return unless handlers
|
|
153
|
+
|
|
154
|
+
handlers.each do |handler|
|
|
155
|
+
begin
|
|
156
|
+
# Execute handler in a new thread to allow it to call stop() without blocking
|
|
157
|
+
handler_thread = Thread.new do
|
|
158
|
+
handler.call(event.data)
|
|
159
|
+
end
|
|
160
|
+
handler_thread.join
|
|
161
|
+
rescue => e
|
|
162
|
+
log_error("Error in event handler: #{e.class} - #{e.message}")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def log_info(message)
|
|
168
|
+
@logger&.info("[NotificationListener] #{message}")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def log_error(message)
|
|
172
|
+
@logger&.error("[NotificationListener] #{message}")
|
|
173
|
+
end
|
|
43
174
|
end
|
|
44
175
|
end
|
|
45
176
|
end
|
data/lib/rox/version.rb
CHANGED
data/rox.gemspec
CHANGED
|
@@ -22,12 +22,12 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
|
|
23
23
|
spec.required_ruby_version = '>= 2.5'
|
|
24
24
|
|
|
25
|
-
spec.add_runtime_dependency '
|
|
25
|
+
spec.add_runtime_dependency 'server_sent_events', '~> 0.1.3'
|
|
26
26
|
|
|
27
|
-
spec.add_development_dependency 'bundler', '~> 2.4
|
|
28
|
-
spec.add_development_dependency 'minitest', '~> 5.
|
|
29
|
-
spec.add_development_dependency 'pry-byebug', '~> 3.
|
|
30
|
-
spec.add_development_dependency 'rake', '~>
|
|
31
|
-
spec.add_development_dependency 'rubocop', '~> 1.
|
|
32
|
-
spec.add_development_dependency 'webmock', '~> 3.
|
|
27
|
+
spec.add_development_dependency 'bundler', '~> 2.4'
|
|
28
|
+
spec.add_development_dependency 'minitest', '~> 5.20'
|
|
29
|
+
spec.add_development_dependency 'pry-byebug', '~> 3.9'
|
|
30
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
31
|
+
spec.add_development_dependency 'rubocop', '~> 1.50'
|
|
32
|
+
spec.add_development_dependency 'webmock', '~> 3.20'
|
|
33
33
|
end
|
metadata
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rox-rollout
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.0.
|
|
4
|
+
version: 6.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CloudBees
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-04-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: server_sent_events
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.3
|
|
19
|
+
version: 0.1.3
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.3
|
|
26
|
+
version: 0.1.3
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: bundler
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 2.4
|
|
33
|
+
version: '2.4'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 2.4
|
|
40
|
+
version: '2.4'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: minitest
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '5.
|
|
47
|
+
version: '5.20'
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '5.
|
|
54
|
+
version: '5.20'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: pry-byebug
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 3.
|
|
61
|
+
version: '3.9'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 3.
|
|
68
|
+
version: '3.9'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: rake
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
73
|
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '
|
|
75
|
+
version: '13.0'
|
|
76
76
|
type: :development
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
82
|
+
version: '13.0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: rubocop
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '1.
|
|
89
|
+
version: '1.50'
|
|
90
90
|
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '1.
|
|
96
|
+
version: '1.50'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
98
|
name: webmock
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
101
|
- - "~>"
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: 3.
|
|
103
|
+
version: '3.20'
|
|
104
104
|
type: :development
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: 3.
|
|
110
|
+
version: '3.20'
|
|
111
111
|
description:
|
|
112
112
|
email:
|
|
113
113
|
- support@rollout.io
|
|
@@ -116,20 +116,22 @@ extensions: []
|
|
|
116
116
|
extra_rdoc_files: []
|
|
117
117
|
files:
|
|
118
118
|
- ".editorconfig"
|
|
119
|
-
- ".github/workflows/e2e_tests.yaml"
|
|
120
|
-
- ".github/workflows/unit_tests.yaml"
|
|
121
119
|
- ".gitignore"
|
|
122
120
|
- ".rubocop.yml"
|
|
123
121
|
- Gemfile
|
|
122
|
+
- Jenkinsfile
|
|
124
123
|
- LICENSE
|
|
125
124
|
- README.md
|
|
126
125
|
- Rakefile
|
|
127
126
|
- bin/console
|
|
128
127
|
- bin/setup
|
|
128
|
+
- cbci-templates/sdkci-e2e.yml
|
|
129
|
+
- cbci-templates/sdkci-unit.yml
|
|
129
130
|
- e2e-server/.gitignore
|
|
130
131
|
- e2e-server/run_server.sh
|
|
131
132
|
- e2e-server/server.rb
|
|
132
133
|
- example/local.rb
|
|
134
|
+
- examples/simple_example.rb
|
|
133
135
|
- lib/rox.rb
|
|
134
136
|
- lib/rox/core/analytics/backoff_policy.rb
|
|
135
137
|
- lib/rox/core/analytics/client.rb
|
|
@@ -248,7 +250,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
248
250
|
- !ruby/object:Gem::Version
|
|
249
251
|
version: '0'
|
|
250
252
|
requirements: []
|
|
251
|
-
rubygems_version: 3.5.
|
|
253
|
+
rubygems_version: 3.5.9
|
|
252
254
|
signing_key:
|
|
253
255
|
specification_version: 4
|
|
254
256
|
summary: Feature Management ROX Ruby SDK
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
name: E2E tests
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- master
|
|
7
|
-
pull_request:
|
|
8
|
-
branches:
|
|
9
|
-
- master
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
build:
|
|
13
|
-
|
|
14
|
-
runs-on: macos-latest
|
|
15
|
-
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v3
|
|
18
|
-
with:
|
|
19
|
-
path: rox-ruby
|
|
20
|
-
- name: Checkout e2e tests
|
|
21
|
-
uses: actions/checkout@v3
|
|
22
|
-
with:
|
|
23
|
-
repository: rollout/sdk-end-2-end-tests
|
|
24
|
-
ref: master
|
|
25
|
-
token: ${{ secrets.E2E_PAT }}
|
|
26
|
-
path: sdk-end-2-end-tests
|
|
27
|
-
- name: link driver
|
|
28
|
-
working-directory: ./sdk-end-2-end-tests/drivers
|
|
29
|
-
run: ln -s $GITHUB_WORKSPACE/rox-ruby/e2e-server ruby
|
|
30
|
-
- name: build e2e node driver
|
|
31
|
-
working-directory: ./sdk-end-2-end-tests/drivers/nodejs
|
|
32
|
-
run: |
|
|
33
|
-
yarn install --frozen-lockfile
|
|
34
|
-
- name: build and run e2e
|
|
35
|
-
working-directory: ./sdk-end-2-end-tests
|
|
36
|
-
run: |
|
|
37
|
-
yarn install --frozen-lockfile
|
|
38
|
-
QA_E2E_BEARER=$QA_E2E_BEARER API_HOST=https://api.test.rollout.io CD_API_ENDPOINT=https://api.test.rollout.io/device/get_configuration CD_S3_ENDPOINT=https://rox-conf.test.rollout.io/ SS_API_ENDPOINT=https://api.test.rollout.io/device/update_state_store/ SS_S3_ENDPOINT=https://rox-state.test.rollout.io/ CLIENT_DATA_CACHE_KEY=client_data ANALYTICS_ENDPOINT=https://analytic.test.rollout.io/ NOTIFICATIONS_ENDPOINT=https://push.test.rollout.io/sse SDK_LANG=ruby NODE_ENV=container yarn test:env
|
|
39
|
-
env:
|
|
40
|
-
QA_E2E_BEARER: ${{ secrets.QA_E2E_BEARER }}
|
|
41
|
-
- name: Show e2e server driver logs
|
|
42
|
-
if: ${{ always() }}
|
|
43
|
-
run: cat ./sdk-end-2-end-tests/drivers/ruby/log_1234.out || echo "no log file"
|
|
44
|
-
- name: Show e2e server sync driver logs
|
|
45
|
-
if: ${{ always() }}
|
|
46
|
-
run: cat ./sdk-end-2-end-tests/drivers/nodejs/log_1233.out || echo "no log file"
|
|
47
|
-
- name: Show e2e server init ClientData driver logs
|
|
48
|
-
if: ${{ always() }}
|
|
49
|
-
run: cat ./sdk-end-2-end-tests/drivers/ruby/log_2234.out || echo "no log file"
|
|
50
|
-
- name: Show e2e server init ClientData sync driver logs
|
|
51
|
-
if: ${{ always() }}
|
|
52
|
-
run: cat ./sdk-end-2-end-tests/drivers/nodejs/log_2233.out || echo "no log file"
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
name: Unit Tests
|
|
2
|
-
|
|
3
|
-
on: [push]
|
|
4
|
-
|
|
5
|
-
jobs:
|
|
6
|
-
test:
|
|
7
|
-
strategy:
|
|
8
|
-
fail-fast: false
|
|
9
|
-
matrix:
|
|
10
|
-
os: [ubuntu-latest]
|
|
11
|
-
ruby: [2.6, 2.7, '3.0', 3.1, 3.2]
|
|
12
|
-
runs-on: ${{ matrix.os }}
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v3
|
|
15
|
-
- uses: ruby/setup-ruby@v1
|
|
16
|
-
with:
|
|
17
|
-
ruby-version: ${{ matrix.ruby }}
|
|
18
|
-
bundler: 2.4.6
|
|
19
|
-
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
20
|
-
- run: bundle exec rake test
|