splitclient-rb 8.11.1.pre.rc3-java → 8.11.2.pre.rc1-java
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 +2 -0
- data/.harness/orgs/PROD/projects/Harness_Split/pipelines/rubyclientrepo/input_sets/rubyclient.yaml +14 -0
- data/.harness/pipeline.yaml +182 -0
- data/.rubocop.yml +1 -1
- data/CHANGES.txt +5 -0
- data/lib/splitclient-rb/engine/push_manager.rb +21 -9
- data/lib/splitclient-rb/engine/reconnect_policy.rb +111 -0
- data/lib/splitclient-rb/engine/sync_manager.rb +49 -39
- data/lib/splitclient-rb/helpers/thread_helper.rb +17 -0
- data/lib/splitclient-rb/sse/event_source/client.rb +128 -53
- data/lib/splitclient-rb/version.rb +1 -1
- data/lib/splitclient-rb.rb +1 -0
- metadata +5 -6
- data/.github/CODEOWNERS +0 -1
- data/.github/pull_request_template.md +0 -9
- data/.github/workflows/ci.yml +0 -90
- data/.github/workflows/update-license-year.yml +0 -45
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7933a7724a5e819fc27a3cab218ccdf752d8d487daee86c2dbab719c4846f784
|
|
4
|
+
data.tar.gz: 76d948cd06f24fc10ccd0030542864360d250d0e6526bd78cdb4441e4dde3aef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e17c51fb4bd3aeb092542d33d8fa87543c59347343eab05e29729c95f6a6104a09b4ffcf65ba09027094d7cd3afecc3be4add56ed27dc8b5dd1b1d1225f73520
|
|
7
|
+
data.tar.gz: 227a956b6f6a67b3eb4567d134dacc1156d547faad08cd2f4d0392ed655d26e6a6a4575b7c15cbe7dc99ebf867284e1967cc4f2825bd8a2bdcd195ce9d45027f
|
data/.gitignore
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
pipeline:
|
|
2
|
+
name: ruby-client-repo
|
|
3
|
+
identifier: rubyclientrepo
|
|
4
|
+
projectIdentifier: Harness_Split
|
|
5
|
+
orgIdentifier: PROD
|
|
6
|
+
tags: {}
|
|
7
|
+
properties:
|
|
8
|
+
ci:
|
|
9
|
+
codebase:
|
|
10
|
+
connectorRef: fmegithubharnessgitops
|
|
11
|
+
repoName: ruby-client
|
|
12
|
+
build:
|
|
13
|
+
type: branch
|
|
14
|
+
spec:
|
|
15
|
+
branch: <+input>
|
|
16
|
+
stages:
|
|
17
|
+
- stage:
|
|
18
|
+
name: Build and Test
|
|
19
|
+
identifier: Build_and_Test
|
|
20
|
+
description: ""
|
|
21
|
+
type: CI
|
|
22
|
+
spec:
|
|
23
|
+
cloneCodebase: true
|
|
24
|
+
caching:
|
|
25
|
+
enabled: true
|
|
26
|
+
override: true
|
|
27
|
+
platform:
|
|
28
|
+
os: Linux
|
|
29
|
+
arch: Amd64
|
|
30
|
+
runtime:
|
|
31
|
+
type: Cloud
|
|
32
|
+
spec:
|
|
33
|
+
size: flex
|
|
34
|
+
execution:
|
|
35
|
+
steps:
|
|
36
|
+
- step:
|
|
37
|
+
type: Run
|
|
38
|
+
name: Install Ruby
|
|
39
|
+
identifier: Install_Ruby
|
|
40
|
+
spec:
|
|
41
|
+
shell: Bash
|
|
42
|
+
command: |-
|
|
43
|
+
curl https://mise.run | sh
|
|
44
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
45
|
+
mise use --global ruby@<+matrix.rubyVersion>
|
|
46
|
+
ruby --version
|
|
47
|
+
gem --version
|
|
48
|
+
- step:
|
|
49
|
+
type: Run
|
|
50
|
+
name: Start Redis
|
|
51
|
+
identifier: Start_Redis
|
|
52
|
+
spec:
|
|
53
|
+
shell: Sh
|
|
54
|
+
command: |-
|
|
55
|
+
apt-get install -y redis-server -qq
|
|
56
|
+
redis-server --daemonize yes --port 6379
|
|
57
|
+
redis-cli ping
|
|
58
|
+
- step:
|
|
59
|
+
type: Run
|
|
60
|
+
name: Wait for Redis
|
|
61
|
+
identifier: Wait_for_Redis
|
|
62
|
+
spec:
|
|
63
|
+
shell: Sh
|
|
64
|
+
command: |-
|
|
65
|
+
for i in $(seq 1 15); do
|
|
66
|
+
redis-cli ping && break
|
|
67
|
+
echo "Waiting for Redis... attempt $i"
|
|
68
|
+
sleep 1
|
|
69
|
+
done
|
|
70
|
+
- step:
|
|
71
|
+
type: Run
|
|
72
|
+
name: Install Dependencies
|
|
73
|
+
identifier: Install_Dependencies
|
|
74
|
+
spec:
|
|
75
|
+
shell: Bash
|
|
76
|
+
command: |-
|
|
77
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
78
|
+
eval "$(mise activate bash)"
|
|
79
|
+
gem install bundler:2.4.19 --no-document
|
|
80
|
+
bundle install --jobs 4 --retry 3
|
|
81
|
+
- step:
|
|
82
|
+
type: Run
|
|
83
|
+
name: Run Tests
|
|
84
|
+
identifier: Run_Tests
|
|
85
|
+
spec:
|
|
86
|
+
shell: Bash
|
|
87
|
+
command: |-
|
|
88
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
89
|
+
eval "$(mise activate bash)"
|
|
90
|
+
bundle exec rake
|
|
91
|
+
- step:
|
|
92
|
+
type: Run
|
|
93
|
+
name: Fix Coverage Paths
|
|
94
|
+
identifier: Fix_Coverage_Paths
|
|
95
|
+
when:
|
|
96
|
+
stageStatus: Success
|
|
97
|
+
condition: <+matrix.rubyVersion> == "3.2.2"
|
|
98
|
+
spec:
|
|
99
|
+
shell: Bash
|
|
100
|
+
command: |-
|
|
101
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
102
|
+
eval "$(mise activate bash)"
|
|
103
|
+
cd coverage
|
|
104
|
+
sed -i "s@${HARNESS_WORKSPACE}@/harness/workspace/@g" .resultset.json
|
|
105
|
+
ruby -rjson -e '
|
|
106
|
+
sqube = JSON.load(File.read(".resultset.json"))["RSpec"]["coverage"].transform_values {|lines| lines["lines"]}
|
|
107
|
+
total = { "RSpec" => { "coverage" => sqube, "timestamp" => Time.now.to_i }}
|
|
108
|
+
puts JSON.dump(total)
|
|
109
|
+
' > .resultset.sonarqube.json
|
|
110
|
+
- step:
|
|
111
|
+
type: Run
|
|
112
|
+
name: SonarQube Scan
|
|
113
|
+
identifier: SonarQube_Scan
|
|
114
|
+
when:
|
|
115
|
+
stageStatus: Success
|
|
116
|
+
condition: <+matrix.rubyVersion> == "3.2.2"
|
|
117
|
+
spec:
|
|
118
|
+
shell: Sh
|
|
119
|
+
command: |-
|
|
120
|
+
export SONAR_SCANNER_VERSION=5.0.1.3006
|
|
121
|
+
curl -sSLo sonar-scanner.zip \
|
|
122
|
+
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux.zip"
|
|
123
|
+
unzip -q sonar-scanner.zip
|
|
124
|
+
export PATH="$PWD/sonar-scanner-${SONAR_SCANNER_VERSION}-linux/bin:$PATH"
|
|
125
|
+
apt-get install -y openjdk-17-jdk -qq
|
|
126
|
+
sonar-scanner \
|
|
127
|
+
-Dsonar.projectKey=ruby-client \
|
|
128
|
+
-Dsonar.sources=lib \
|
|
129
|
+
-Dsonar.tests=spec \
|
|
130
|
+
-Dsonar.host.url=https://sonar.harness.io \
|
|
131
|
+
-Dsonar.token=<+secrets.getValue("sonarqube-token")> \
|
|
132
|
+
-Dsonar.ruby.coverage.reportPaths=coverage/.resultset.sonarqube.json \
|
|
133
|
+
-Dsonar.coverage.exclusions="spec/**,**/vendor/**" \
|
|
134
|
+
-Dsonar.exclusions="**/vendor/**,**/.git/**"
|
|
135
|
+
- step:
|
|
136
|
+
type: Run
|
|
137
|
+
name: Post Quality Gate
|
|
138
|
+
identifier: Post_Quality_Gate
|
|
139
|
+
when:
|
|
140
|
+
stageStatus: Success
|
|
141
|
+
condition: <+matrix.rubyVersion> == "3.2.2"
|
|
142
|
+
spec:
|
|
143
|
+
shell: Bash
|
|
144
|
+
command: |-
|
|
145
|
+
#!/bin/bash
|
|
146
|
+
set -e
|
|
147
|
+
|
|
148
|
+
SONAR_TOKEN="<+secrets.getValue("sonarqube-token")>"
|
|
149
|
+
SONAR_URL="https://sonar.harness.io"
|
|
150
|
+
SONAR_PROJECT_KEY="ruby-client"
|
|
151
|
+
GITHUB_TOKEN="<+secrets.getValue("github-devops-token")>"
|
|
152
|
+
GITHUB_REPO="ruby-client"
|
|
153
|
+
COMMIT_SHA="<+codebase.commitSha>"
|
|
154
|
+
|
|
155
|
+
STATUS_JSON=$(curl -sf \
|
|
156
|
+
-H "Authorization: Bearer ${SONAR_TOKEN}" \
|
|
157
|
+
"${SONAR_URL}/api/qualitygates/project_status?projectKey=${SONAR_PROJECT_KEY}")
|
|
158
|
+
|
|
159
|
+
QG_STATUS=$(echo "$STATUS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['projectStatus']['status'])")
|
|
160
|
+
|
|
161
|
+
case "$QG_STATUS" in
|
|
162
|
+
OK) GH_STATE="success"; DESCRIPTION="Quality gate passed" ;;
|
|
163
|
+
ERROR) GH_STATE="failure"; DESCRIPTION="Quality gate failed" ;;
|
|
164
|
+
WARN) GH_STATE="success"; DESCRIPTION="Quality gate passed with warnings" ;;
|
|
165
|
+
*) GH_STATE="pending"; DESCRIPTION="Quality gate status unknown" ;;
|
|
166
|
+
esac
|
|
167
|
+
|
|
168
|
+
TARGET_URL="${SONAR_URL}/dashboard?id=${SONAR_PROJECT_KEY}"
|
|
169
|
+
|
|
170
|
+
curl -sf -X POST \
|
|
171
|
+
-H "Authorization: token ${GITHUB_TOKEN}" \
|
|
172
|
+
-H "Accept: application/vnd.github.v3+json" \
|
|
173
|
+
-H "Content-Type: application/json" \
|
|
174
|
+
-d "{\"state\":\"${GH_STATE}\",\"description\":\"${DESCRIPTION}\",\"context\":\"sonarqube/quality-gate\",\"target_url\":\"${TARGET_URL}\"}" \
|
|
175
|
+
"https://api.github.com/repos/splitio/${GITHUB_REPO}/statuses/${COMMIT_SHA}"
|
|
176
|
+
|
|
177
|
+
echo "Posted SonarQube quality gate status: ${GH_STATE}"
|
|
178
|
+
strategy:
|
|
179
|
+
matrix:
|
|
180
|
+
rubyVersion:
|
|
181
|
+
- "2.7.8"
|
|
182
|
+
- "3.2.2"
|
data/.rubocop.yml
CHANGED
data/CHANGES.txt
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
CHANGES
|
|
2
2
|
|
|
3
|
+
8.11.1 (Jul, 23, 2026)
|
|
4
|
+
- Added a retry mechanism in case the initial SSE socket connection failed, and extended the list of retryable errors when socket exceptions are caught during streaming.
|
|
5
|
+
- Fixed debug logging in rule-based segment matcher class when debug_enabled parameter is not set in configuration .
|
|
6
|
+
- Fixed a debug log that caused exception when streaming socket closes.
|
|
7
|
+
|
|
3
8
|
8.11.0 (Mar, 12, 2026)
|
|
4
9
|
- Added the ability to listen to different events triggered by the SDK. Read more in our docs.
|
|
5
10
|
- SDK_UPDATE notify when a flag or user segment has changed
|
|
@@ -10,9 +10,25 @@ module SplitIoClient
|
|
|
10
10
|
@api_key = api_key
|
|
11
11
|
@back_off = Engine::BackOff.new(@config.auth_retry_back_off_base, 1)
|
|
12
12
|
@telemetry_runtime_producer = telemetry_runtime_producer
|
|
13
|
+
# Serializes start_sse: it is reachable from the push status handler thread and
|
|
14
|
+
# from the token refresh thread concurrently.
|
|
15
|
+
@start_sse_mutex = Mutex.new
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
def start_sse
|
|
19
|
+
@start_sse_mutex.synchronize { start_sse_unsynchronized }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def stop_sse
|
|
23
|
+
@sse_handler.stop
|
|
24
|
+
SplitIoClient::Helpers::ThreadHelper.stop(:schedule_next_token_refresh, @config)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
@config.logger.error(e.inspect)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def start_sse_unsynchronized
|
|
16
32
|
response = @auth_api_client.authenticate(@api_key)
|
|
17
33
|
@config.logger.debug("Auth service response push_enabled: #{response[:push_enabled]}") if @config.debug_enabled
|
|
18
34
|
|
|
@@ -33,18 +49,14 @@ module SplitIoClient
|
|
|
33
49
|
true
|
|
34
50
|
rescue StandardError => e
|
|
35
51
|
@config.logger.error("start_sse: #{e.inspect}")
|
|
52
|
+
false
|
|
36
53
|
end
|
|
37
54
|
|
|
38
|
-
def stop_sse
|
|
39
|
-
@sse_handler.stop
|
|
40
|
-
SplitIoClient::Helpers::ThreadHelper.stop(:schedule_next_token_refresh, @config)
|
|
41
|
-
rescue StandardError => e
|
|
42
|
-
@config.logger.error(e.inspect)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
55
|
def schedule_next_token_refresh(time)
|
|
56
|
+
# Reap first: start_sse is reachable both from the push status handler thread and
|
|
57
|
+
# from refresh_token_task itself, so without this an orphaned timer thread can
|
|
58
|
+
# survive with no handle to cancel it and fire its own reconnect later.
|
|
59
|
+
SplitIoClient::Helpers::ThreadHelper.reap(:schedule_next_token_refresh, @config)
|
|
48
60
|
@config.threads[:schedule_next_token_refresh] = Thread.new { refresh_token_task(time) }
|
|
49
61
|
end
|
|
50
62
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SplitIoClient
|
|
4
|
+
module Engine
|
|
5
|
+
# Governs how aggressively streaming reconnects are attempted.
|
|
6
|
+
#
|
|
7
|
+
# Extracted for FME-18143, where a customer's SDK reconnected up to ~64 times per
|
|
8
|
+
# second and accumulated ~10,000 threads. Two properties matter here:
|
|
9
|
+
#
|
|
10
|
+
# * A connection only earns a back off reset if it stayed up long enough to count
|
|
11
|
+
# as a genuine success. Resetting on every PUSH_CONNECTED meant a connection
|
|
12
|
+
# that lived for milliseconds still produced a zero second retry.
|
|
13
|
+
# * Only one reconnect may be in flight at a time, and errors describing an
|
|
14
|
+
# already-replaced connection must not each start another one.
|
|
15
|
+
class ReconnectPolicy
|
|
16
|
+
# How long a connection must survive before it counts as stable.
|
|
17
|
+
MIN_UPTIME_FOR_BACKOFF_RESET = 30
|
|
18
|
+
|
|
19
|
+
def initialize(config, status_queue, back_off = nil)
|
|
20
|
+
@config = config
|
|
21
|
+
@status_queue = status_queue
|
|
22
|
+
@back_off = back_off || Engine::BackOff.new(1, 3)
|
|
23
|
+
@connected_at = Concurrent::AtomicReference.new(nil)
|
|
24
|
+
@reconnecting = Concurrent::AtomicBoolean.new(false)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Monotonic clock: a backwards wall-clock jump (NTP correction, container
|
|
28
|
+
# migration) must not make uptime go negative and permanently block the
|
|
29
|
+
# back off reset.
|
|
30
|
+
def connected
|
|
31
|
+
@connected_at.set(monotonic_now)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether a disconnect should be acted on at all. Returns false for a disconnect
|
|
35
|
+
# of an already-disconnected stream, and for a reconnect when one is already in
|
|
36
|
+
# flight (typically driven by the token refresh thread) -- running two
|
|
37
|
+
# concurrently is what multiplied connection attempts.
|
|
38
|
+
def actionable?(sse_connected, reconnect)
|
|
39
|
+
unless sse_connected || reconnect
|
|
40
|
+
log_debug('Streaming already disconnected.')
|
|
41
|
+
return false
|
|
42
|
+
end
|
|
43
|
+
return true unless reconnect && !acquire
|
|
44
|
+
|
|
45
|
+
log_debug('Streaming reconnect already in progress, ignoring.')
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns true only for the caller that acquired the right to reconnect; a
|
|
50
|
+
# concurrent caller gets false. AtomicBoolean#make_true is the compare-and-set:
|
|
51
|
+
# it returns true only for the transition from false to true.
|
|
52
|
+
def acquire
|
|
53
|
+
@reconnecting.make_true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def release
|
|
57
|
+
@reconnecting.make_false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Resets the back off only if the connection that just dropped was stable, so a
|
|
61
|
+
# flapping connection keeps backing off instead of retrying immediately.
|
|
62
|
+
def record_disconnect
|
|
63
|
+
connected_at = @connected_at.get_and_set(nil)
|
|
64
|
+
return if connected_at.nil?
|
|
65
|
+
|
|
66
|
+
uptime = monotonic_now - connected_at
|
|
67
|
+
return if uptime < MIN_UPTIME_FOR_BACKOFF_RESET
|
|
68
|
+
|
|
69
|
+
log_debug("Streaming connection was up for #{uptime.round(1)}s, resetting back off.")
|
|
70
|
+
@back_off.reset
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def interval
|
|
74
|
+
@back_off.interval
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Drops queued retryable errors, which refer to the connection just replaced.
|
|
78
|
+
# Acting on each of them would start one more reconnect apiece. Other statuses
|
|
79
|
+
# are preserved in order.
|
|
80
|
+
def discard_stale_errors
|
|
81
|
+
kept = []
|
|
82
|
+
discarded = 0
|
|
83
|
+
|
|
84
|
+
until @status_queue.empty?
|
|
85
|
+
status = @status_queue.pop(true)
|
|
86
|
+
if status == Constants::PUSH_RETRYABLE_ERROR
|
|
87
|
+
discarded += 1
|
|
88
|
+
else
|
|
89
|
+
kept << status
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
kept.each { |kept_status| @status_queue.push(kept_status) }
|
|
94
|
+
log_debug("Discarded #{discarded} stale streaming errors.") if discarded.positive?
|
|
95
|
+
discarded
|
|
96
|
+
rescue ThreadError
|
|
97
|
+
discarded
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def log_debug(message)
|
|
103
|
+
@config.logger.debug(message) if @config.debug_enabled
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def monotonic_now
|
|
107
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -17,7 +17,7 @@ module SplitIoClient
|
|
|
17
17
|
@push_manager = push_manager
|
|
18
18
|
@status_queue = status_queue
|
|
19
19
|
@sse_connected = Concurrent::AtomicBoolean.new(false)
|
|
20
|
-
@
|
|
20
|
+
@reconnect_policy = Engine::ReconnectPolicy.new(config, status_queue)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def start
|
|
@@ -110,30 +110,37 @@ module SplitIoClient
|
|
|
110
110
|
@synchronizer.start_periodic_fetch
|
|
111
111
|
record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_POLLING)
|
|
112
112
|
rescue StandardError => e
|
|
113
|
-
@config.logger.error("
|
|
113
|
+
@config.logger.error("process_forced_stop error: #{e.inspect}")
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def process_disconnect(reconnect)
|
|
117
|
-
unless @sse_connected.value
|
|
118
|
-
@config.logger.debug('Streaming already disconnected.') if @config.debug_enabled
|
|
119
|
-
return
|
|
120
|
-
end
|
|
117
|
+
return unless @reconnect_policy.actionable?(@sse_connected.value, reconnect)
|
|
121
118
|
|
|
119
|
+
@reconnect_policy.record_disconnect
|
|
122
120
|
@sse_connected.make_false
|
|
123
121
|
@sse_handler.stop_workers
|
|
124
122
|
@synchronizer.start_periodic_fetch
|
|
125
123
|
record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_POLLING)
|
|
126
124
|
|
|
127
|
-
if reconnect
|
|
128
|
-
wait_interval = @back_off.interval
|
|
129
|
-
@config.logger.debug("Retrying streaming connection in: #{wait_interval} seconds")
|
|
130
|
-
sleep(wait_interval)
|
|
131
|
-
@push_manager.stop_sse
|
|
132
|
-
@synchronizer.sync_all
|
|
133
|
-
@push_manager.start_sse
|
|
134
|
-
end
|
|
125
|
+
reconnect_streaming if reconnect
|
|
135
126
|
rescue StandardError => e
|
|
136
127
|
@config.logger.error("process_disconnect error: #{e.inspect}")
|
|
128
|
+
@reconnect_policy.release if reconnect
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def reconnect_streaming
|
|
132
|
+
wait_interval = @reconnect_policy.interval
|
|
133
|
+
log_if_debug("Retrying streaming connection in: #{wait_interval} seconds")
|
|
134
|
+
sleep(wait_interval)
|
|
135
|
+
@push_manager.stop_sse
|
|
136
|
+
@synchronizer.sync_all
|
|
137
|
+
|
|
138
|
+
# Only discard queued errors once streaming is actually back up. If the retry
|
|
139
|
+
# failed, those errors are still the only thing that will drive the next
|
|
140
|
+
# attempt, so they must be left alone.
|
|
141
|
+
@reconnect_policy.discard_stale_errors if @push_manager.start_sse
|
|
142
|
+
ensure
|
|
143
|
+
@reconnect_policy.release
|
|
137
144
|
end
|
|
138
145
|
|
|
139
146
|
def record_telemetry(type, data)
|
|
@@ -148,36 +155,39 @@ module SplitIoClient
|
|
|
148
155
|
end
|
|
149
156
|
|
|
150
157
|
def incoming_push_status_handler
|
|
158
|
+
# Rescue lives inside the loop: a bug in any handler (including one
|
|
159
|
+
# that lacks its own rescue) would otherwise exit the loop and
|
|
160
|
+
# permanently stop the SDK from reacting to push status changes.
|
|
151
161
|
while (status = @status_queue.pop)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
process_subsystem_down
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
162
|
+
begin
|
|
163
|
+
@config.logger.debug("Push status handler dequeue #{status}") if @config.debug_enabled
|
|
164
|
+
case status
|
|
165
|
+
when Constants::PUSH_CONNECTED
|
|
166
|
+
# Deliberately not resetting the back off here. Whether this
|
|
167
|
+
# connection earns a reset depends on how long it survives,
|
|
168
|
+
# which is only known once it drops -- see
|
|
169
|
+
# ReconnectPolicy#record_disconnect.
|
|
170
|
+
@reconnect_policy.connected
|
|
171
|
+
process_connected
|
|
172
|
+
when Constants::PUSH_RETRYABLE_ERROR then process_disconnect(true)
|
|
173
|
+
when Constants::PUSH_FORCED_STOP then process_forced_stop
|
|
174
|
+
when Constants::PUSH_NONRETRYABLE_ERROR then process_disconnect(false)
|
|
175
|
+
when Constants::PUSH_SUBSYSTEM_DOWN then process_subsystem_down
|
|
176
|
+
when Constants::PUSH_SUBSYSTEM_READY then process_subsystem_ready
|
|
177
|
+
when Constants::PUSH_SUBSYSTEM_OFF then process_push_shutdown
|
|
178
|
+
else log_if_debug('Incorrect push status type.')
|
|
179
|
+
end
|
|
180
|
+
rescue StandardError => e
|
|
181
|
+
@config.logger.error("Push status handler error for #{status}: #{e.inspect}")
|
|
172
182
|
end
|
|
173
183
|
end
|
|
174
|
-
rescue StandardError => e
|
|
175
|
-
@config.logger.error("Push status handler error: #{e.inspect}")
|
|
176
184
|
end
|
|
177
|
-
end
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
# Was defined on the enclosing module, so every call raised NoMethodError and
|
|
187
|
+
# killed the push status handler thread (its rescue sits outside the loop).
|
|
188
|
+
def log_if_debug(msg)
|
|
189
|
+
@config.logger.debug(msg) if @config.debug_enabled
|
|
190
|
+
end
|
|
181
191
|
end
|
|
182
192
|
end
|
|
183
193
|
end
|
|
@@ -6,6 +6,10 @@ module SplitIoClient
|
|
|
6
6
|
def self.stop(thread_sym, config)
|
|
7
7
|
thread = config.threads[thread_sym]
|
|
8
8
|
|
|
9
|
+
# Never kill the calling thread: stop_sse is reachable from inside the token
|
|
10
|
+
# refresh thread, which would otherwise terminate itself mid-flight.
|
|
11
|
+
return if thread == Thread.current
|
|
12
|
+
|
|
9
13
|
unless thread.nil?
|
|
10
14
|
config.logger.debug("Stopping #{thread_sym} thread...") if config.debug_enabled
|
|
11
15
|
Thread.kill(thread)
|
|
@@ -14,6 +18,19 @@ module SplitIoClient
|
|
|
14
18
|
config.logger.error(e.inspect)
|
|
15
19
|
end
|
|
16
20
|
|
|
21
|
+
# Terminates the thread currently held in +thread_sym+ before its handle is
|
|
22
|
+
# replaced. Thread handles live in a single slot per symbol, so overwriting one
|
|
23
|
+
# while the thread is still running makes it unreachable and leaks it forever.
|
|
24
|
+
def self.reap(thread_sym, config, timeout = 1)
|
|
25
|
+
thread = config.threads[thread_sym]
|
|
26
|
+
return if thread.nil? || thread == Thread.current || !thread.alive?
|
|
27
|
+
|
|
28
|
+
config.logger.warn("#{thread_sym} thread is still alive, terminating it before starting a new one.")
|
|
29
|
+
Thread.kill(thread) unless thread.join(timeout)
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
config.logger.error(e.inspect)
|
|
32
|
+
end
|
|
33
|
+
|
|
17
34
|
def self.alive?(thread_sym, config)
|
|
18
35
|
thread = config.threads[thread_sym]
|
|
19
36
|
|
|
@@ -32,20 +32,36 @@ module SplitIoClient
|
|
|
32
32
|
@status_queue = status_queue
|
|
33
33
|
@read_timeout = read_timeout
|
|
34
34
|
@connected = Concurrent::AtomicBoolean.new(false)
|
|
35
|
-
|
|
35
|
+
# Set while close is tearing a connection down on purpose, so the reader
|
|
36
|
+
# thread does not report the resulting socket error as a retryable failure.
|
|
37
|
+
@shutdown = Concurrent::AtomicBoolean.new(false)
|
|
38
|
+
# Incremented for every connection attempt. A reader thread whose generation
|
|
39
|
+
# is no longer current has been superseded and must exit without side effects.
|
|
40
|
+
@generation = Concurrent::AtomicFixnum.new(0)
|
|
36
41
|
@socket = nil
|
|
42
|
+
# Serialises concurrent close calls so the second caller does not
|
|
43
|
+
# re-push the status or double-close a socket the first caller
|
|
44
|
+
# already claimed.
|
|
45
|
+
@close_mutex = Mutex.new
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
def close(status = nil)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
socket = nil
|
|
50
|
+
@close_mutex.synchronize do
|
|
51
|
+
return if @socket.nil?
|
|
52
|
+
|
|
53
|
+
@config.logger.debug('Closing SSEClient socket') if @config.debug_enabled
|
|
54
|
+
@shutdown.make_true
|
|
55
|
+
push_status(status)
|
|
56
|
+
@connected.make_false
|
|
57
|
+
|
|
58
|
+
socket = @socket
|
|
59
|
+
@socket = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
socket.sync_close = true if socket.is_a? OpenSSL::SSL::SSLSocket
|
|
63
|
+
socket.close
|
|
64
|
+
@config.logger.debug("SSEClient socket state #{socket.state}") if socket.is_a?(OpenSSL::SSL::SSLSocket) && @config.debug_enabled
|
|
49
65
|
rescue StandardError => e
|
|
50
66
|
@config.logger.error("SSEClient close Error: #{e.inspect}")
|
|
51
67
|
end
|
|
@@ -60,7 +76,10 @@ module SplitIoClient
|
|
|
60
76
|
latch = Concurrent::CountDownLatch.new(1)
|
|
61
77
|
connect_thread(latch)
|
|
62
78
|
|
|
63
|
-
|
|
79
|
+
unless latch.wait(CONNECT_TIMEOUT)
|
|
80
|
+
@config.logger.warn("SSE connection attempt did not complete within #{CONNECT_TIMEOUT} seconds.")
|
|
81
|
+
return false
|
|
82
|
+
end
|
|
64
83
|
|
|
65
84
|
connected?
|
|
66
85
|
rescue StandardError => e
|
|
@@ -75,95 +94,143 @@ module SplitIoClient
|
|
|
75
94
|
private
|
|
76
95
|
|
|
77
96
|
def connect_thread(latch)
|
|
97
|
+
# Never orphan the previous reader thread: its handle lives in a single slot
|
|
98
|
+
# and would otherwise become unreachable, leaking the thread forever.
|
|
99
|
+
Helpers::ThreadHelper.reap(:connect_stream, @config)
|
|
100
|
+
|
|
101
|
+
generation = @generation.increment
|
|
102
|
+
@shutdown.make_false
|
|
103
|
+
|
|
78
104
|
@config.threads[:connect_stream] = Thread.new do
|
|
79
105
|
@config.logger.info('Starting connect_stream thread ...')
|
|
80
|
-
new_status = connect_stream(latch)
|
|
106
|
+
new_status = connect_stream(latch, generation)
|
|
81
107
|
push_status(new_status) unless new_status.nil?
|
|
82
108
|
@config.logger.info('connect_stream thread finished.')
|
|
83
109
|
end
|
|
84
110
|
end
|
|
85
111
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
112
|
+
# Each reader thread owns the socket it opened. Nothing about the connection is
|
|
113
|
+
# shared with later attempts, so a superseded thread can neither read from nor
|
|
114
|
+
# steal bytes off a socket belonging to a newer connection.
|
|
115
|
+
def connect_stream(latch, generation = nil)
|
|
116
|
+
generation ||= @generation.value
|
|
117
|
+
socket = open_socket(latch)
|
|
118
|
+
return retryable_error(generation) if socket.nil?
|
|
119
|
+
|
|
120
|
+
first_event = true
|
|
121
|
+
|
|
122
|
+
while current_generation?(generation)
|
|
123
|
+
begin
|
|
124
|
+
if IO.select([socket], nil, nil, @read_timeout)
|
|
91
125
|
begin
|
|
92
|
-
partial_data =
|
|
126
|
+
partial_data = socket.readpartial(10_000)
|
|
93
127
|
|
|
94
|
-
|
|
95
|
-
|
|
128
|
+
if first_event
|
|
129
|
+
first_event = false
|
|
130
|
+
first_event_status = read_first_event(partial_data, latch, generation)
|
|
131
|
+
return first_event_status unless first_event_status.nil?
|
|
132
|
+
end
|
|
96
133
|
rescue IO::WaitReadable => e
|
|
97
134
|
@config.logger.debug("SSE client IO::WaitReadable transient error: #{e.inspect}") if @config.debug_enabled
|
|
98
|
-
IO.select([
|
|
135
|
+
IO.select([socket], nil, nil, @read_timeout)
|
|
99
136
|
retry
|
|
100
|
-
rescue
|
|
137
|
+
rescue Errno::EAGAIN => e
|
|
101
138
|
@config.logger.debug("SSE client transient error: #{e.inspect}") if @config.debug_enabled
|
|
102
|
-
IO.select([
|
|
139
|
+
IO.select([socket], nil, nil, @read_timeout)
|
|
103
140
|
retry
|
|
104
141
|
rescue Errno::ETIMEDOUT => e
|
|
105
142
|
@config.logger.error("SSE read operation timed out!: #{e.inspect}")
|
|
106
|
-
return
|
|
143
|
+
return retryable_error(generation)
|
|
107
144
|
rescue EOFError => e
|
|
108
145
|
@config.logger.error("SSE read operation EOF, server closed the connection, will reconnect: #{e.inspect}")
|
|
109
|
-
return
|
|
146
|
+
return retryable_error(generation)
|
|
110
147
|
rescue Errno::EBADF, IOError => e
|
|
111
148
|
@config.logger.error("SSE read operation EBADF or IOError: #{e.inspect}")
|
|
112
|
-
return
|
|
149
|
+
return retryable_error(generation)
|
|
113
150
|
rescue StandardError => e
|
|
114
151
|
@config.logger.error("SSE read operation StandardError: #{e.inspect}")
|
|
115
152
|
return nil if ENV['SPLITCLIENT_ENV'] == 'test'
|
|
116
153
|
|
|
117
154
|
@config.logger.error("Error reading partial data: #{e.inspect}")
|
|
118
|
-
return
|
|
155
|
+
return retryable_error(generation)
|
|
119
156
|
end
|
|
120
157
|
else
|
|
121
|
-
@config.logger.error(
|
|
122
|
-
return
|
|
158
|
+
@config.logger.error('SSE read operation timed out, no data available.')
|
|
159
|
+
return retryable_error(generation)
|
|
123
160
|
end
|
|
124
|
-
rescue Exception
|
|
125
|
-
|
|
126
|
-
|
|
161
|
+
# Was `rescue Exception` logging at debug level only. That combination hid
|
|
162
|
+
# the reason for ~14,880 of ~15,000 reconnects during the FME-18143 incident.
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
@config.logger.warn("SSE socket is not connected: #{e.inspect}")
|
|
165
|
+
return retryable_error(generation)
|
|
127
166
|
end
|
|
128
167
|
|
|
129
168
|
process_data(partial_data)
|
|
130
169
|
end
|
|
131
|
-
@config.logger.info("SSE read operation exited: #{connected?}")
|
|
132
170
|
|
|
171
|
+
@config.logger.debug('connect_stream superseded by a newer connection, exiting.') if @config.debug_enabled
|
|
133
172
|
nil
|
|
134
173
|
end
|
|
135
174
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
175
|
+
# Single decision point for "should this thread ask SyncManager to reconnect?".
|
|
176
|
+
# An intentional close, or a connection that has already been superseded, must
|
|
177
|
+
# stay silent -- reporting either as a retryable error is what turned one
|
|
178
|
+
# dropped connection into a self-sustaining reconnect storm.
|
|
179
|
+
def retryable_error(generation)
|
|
180
|
+
unless report_error?(generation)
|
|
181
|
+
@config.logger.debug('SSE connection ended intentionally, not requesting a reconnect.') if @config.debug_enabled
|
|
182
|
+
return nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
Constants::PUSH_RETRYABLE_ERROR
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def report_error?(generation)
|
|
189
|
+
current_generation?(generation) && !@shutdown.value
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def current_generation?(generation)
|
|
193
|
+
generation == @generation.value
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def open_socket(latch)
|
|
197
|
+
socket = socket_connect
|
|
198
|
+
raise IOError, "unable to open socket to #{@uri.host}" if socket.nil?
|
|
199
|
+
|
|
200
|
+
@socket = socket
|
|
201
|
+
socket.puts(build_request(@uri))
|
|
202
|
+
socket
|
|
141
203
|
rescue StandardError => e
|
|
142
204
|
@config.logger.error("Error during connecting to #{@uri.host}. Error: #{e.inspect}")
|
|
205
|
+
close_quietly(socket)
|
|
206
|
+
@socket = nil if @socket.equal?(socket)
|
|
143
207
|
latch.count_down
|
|
144
|
-
|
|
208
|
+
nil
|
|
145
209
|
end
|
|
146
210
|
|
|
147
|
-
def read_first_event(data, latch)
|
|
148
|
-
|
|
149
|
-
|
|
211
|
+
def read_first_event(data, latch, generation = nil)
|
|
212
|
+
generation ||= @generation.value
|
|
150
213
|
response_code = @event_parser.first_event(data)
|
|
151
214
|
@config.logger.debug("SSE client first event code: #{response_code}") if @config.debug_enabled
|
|
152
215
|
|
|
153
|
-
@first_event.make_false
|
|
154
|
-
|
|
155
216
|
if response_code != OK_CODE
|
|
156
217
|
@config.logger.error("SSE first event failed, code: #{response_code}")
|
|
157
218
|
latch.count_down
|
|
158
|
-
return
|
|
219
|
+
return retryable_error(generation)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Only the connection that is still current may announce itself as connected.
|
|
223
|
+
if current_generation?(generation)
|
|
224
|
+
@connected.make_true
|
|
225
|
+
@config.logger.debug('SSE client first event Connected is true') if @config.debug_enabled
|
|
226
|
+
@telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::SSE_CONNECTION_ESTABLISHED, nil)
|
|
227
|
+
push_status(Constants::PUSH_CONNECTED)
|
|
228
|
+
elsif @config.debug_enabled
|
|
229
|
+
@config.logger.debug('Ignoring first event from a superseded SSE connection.')
|
|
159
230
|
end
|
|
160
231
|
|
|
161
|
-
@connected.make_true
|
|
162
|
-
@config.logger.debug("SSE client first event Connected is true") if @config.debug_enabled
|
|
163
|
-
@telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::SSE_CONNECTION_ESTABLISHED, nil)
|
|
164
|
-
push_status(Constants::PUSH_CONNECTED)
|
|
165
232
|
latch.count_down
|
|
166
|
-
|
|
233
|
+
nil
|
|
167
234
|
end
|
|
168
235
|
|
|
169
236
|
def socket_connect
|
|
@@ -184,9 +251,11 @@ module SplitIoClient
|
|
|
184
251
|
retry
|
|
185
252
|
end
|
|
186
253
|
return ssl_socket
|
|
187
|
-
|
|
188
|
-
rescue Exception => e
|
|
254
|
+
rescue StandardError => e
|
|
189
255
|
@config.logger.error("socket connect error: #{e.inspect}")
|
|
256
|
+
# Without this the underlying descriptor is leaked on every failed
|
|
257
|
+
# attempt, which exhausts file descriptors during a reconnect storm.
|
|
258
|
+
close_quietly(tcp_socket)
|
|
190
259
|
return nil
|
|
191
260
|
end
|
|
192
261
|
end
|
|
@@ -194,6 +263,12 @@ module SplitIoClient
|
|
|
194
263
|
tcp_socket
|
|
195
264
|
end
|
|
196
265
|
|
|
266
|
+
def close_quietly(socket)
|
|
267
|
+
socket.close unless socket.nil? || socket.closed?
|
|
268
|
+
rescue StandardError => e
|
|
269
|
+
@config.logger.debug("Error closing socket: #{e.inspect}") if @config.debug_enabled
|
|
270
|
+
end
|
|
271
|
+
|
|
197
272
|
def process_data(partial_data)
|
|
198
273
|
@config.logger.debug("Event partial data: #{partial_data}") if @config.debug_enabled
|
|
199
274
|
return if partial_data.nil? || partial_data == KEEP_ALIVE_RESPONSE
|
|
@@ -247,7 +322,7 @@ module SplitIoClient
|
|
|
247
322
|
|
|
248
323
|
def push_status(status)
|
|
249
324
|
return if status.nil?
|
|
250
|
-
|
|
325
|
+
|
|
251
326
|
@config.logger.debug("Pushing new sse status: #{status}") if @config.debug_enabled
|
|
252
327
|
@status_queue.push(status)
|
|
253
328
|
end
|
data/lib/splitclient-rb.rb
CHANGED
|
@@ -126,6 +126,7 @@ require 'splitclient-rb/engine/models/valid_sdk_event.rb'
|
|
|
126
126
|
require 'splitclient-rb/engine/models/event_active_subscriptions.rb'
|
|
127
127
|
require 'splitclient-rb/engine/auth_api_client'
|
|
128
128
|
require 'splitclient-rb/engine/back_off'
|
|
129
|
+
require 'splitclient-rb/engine/reconnect_policy'
|
|
129
130
|
require 'splitclient-rb/engine/fallback_treatment_calculator.rb'
|
|
130
131
|
require 'splitclient-rb/engine/push_manager'
|
|
131
132
|
require 'splitclient-rb/engine/status_manager'
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: splitclient-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.11.
|
|
4
|
+
version: 8.11.2.pre.rc1
|
|
5
5
|
platform: java
|
|
6
6
|
authors:
|
|
7
7
|
- Split Software
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-08-21 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: allocation_stats
|
|
@@ -382,11 +382,9 @@ executables: []
|
|
|
382
382
|
extensions: []
|
|
383
383
|
extra_rdoc_files: []
|
|
384
384
|
files:
|
|
385
|
-
- ".github/CODEOWNERS"
|
|
386
|
-
- ".github/pull_request_template.md"
|
|
387
|
-
- ".github/workflows/ci.yml"
|
|
388
|
-
- ".github/workflows/update-license-year.yml"
|
|
389
385
|
- ".gitignore"
|
|
386
|
+
- ".harness/orgs/PROD/projects/Harness_Split/pipelines/rubyclientrepo/input_sets/rubyclient.yaml"
|
|
387
|
+
- ".harness/pipeline.yaml"
|
|
390
388
|
- ".rubocop.yml"
|
|
391
389
|
- ".simplecov"
|
|
392
390
|
- CHANGES.txt
|
|
@@ -513,6 +511,7 @@ files:
|
|
|
513
511
|
- lib/splitclient-rb/engine/parser/evaluator.rb
|
|
514
512
|
- lib/splitclient-rb/engine/parser/partition.rb
|
|
515
513
|
- lib/splitclient-rb/engine/push_manager.rb
|
|
514
|
+
- lib/splitclient-rb/engine/reconnect_policy.rb
|
|
516
515
|
- lib/splitclient-rb/engine/status_manager.rb
|
|
517
516
|
- lib/splitclient-rb/engine/sync_manager.rb
|
|
518
517
|
- lib/splitclient-rb/engine/synchronizer.rb
|
data/.github/CODEOWNERS
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
* @splitio/sdk
|
data/.github/workflows/ci.yml
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
name: ci
|
|
2
|
-
on:
|
|
3
|
-
pull_request:
|
|
4
|
-
branches:
|
|
5
|
-
- development
|
|
6
|
-
- master
|
|
7
|
-
push:
|
|
8
|
-
branches:
|
|
9
|
-
- development
|
|
10
|
-
- master
|
|
11
|
-
|
|
12
|
-
concurrency:
|
|
13
|
-
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
14
|
-
cancel-in-progress: true
|
|
15
|
-
|
|
16
|
-
jobs:
|
|
17
|
-
test:
|
|
18
|
-
name: Test
|
|
19
|
-
runs-on: ubuntu-latest
|
|
20
|
-
services:
|
|
21
|
-
redis:
|
|
22
|
-
image: redis
|
|
23
|
-
ports:
|
|
24
|
-
- 6379:6379
|
|
25
|
-
strategy:
|
|
26
|
-
fail-fast: false
|
|
27
|
-
matrix:
|
|
28
|
-
version:
|
|
29
|
-
- '2.5.0'
|
|
30
|
-
- '3.2.2'
|
|
31
|
-
|
|
32
|
-
steps:
|
|
33
|
-
- name: Checkout code
|
|
34
|
-
uses: actions/checkout@v3
|
|
35
|
-
with:
|
|
36
|
-
fetch-depth: 0
|
|
37
|
-
|
|
38
|
-
- name: Set up Java
|
|
39
|
-
uses: actions/setup-java@v2
|
|
40
|
-
with:
|
|
41
|
-
java-version: 17
|
|
42
|
-
distribution: "temurin"
|
|
43
|
-
|
|
44
|
-
- name: Setup Ruby ${{ matrix.version }}
|
|
45
|
-
uses: ruby/setup-ruby@v1
|
|
46
|
-
with:
|
|
47
|
-
ruby-version: ${{ matrix.version }}
|
|
48
|
-
|
|
49
|
-
- name: Install dependencies
|
|
50
|
-
run: bundle install
|
|
51
|
-
|
|
52
|
-
- name: Run tests
|
|
53
|
-
run: bundle exec rake
|
|
54
|
-
|
|
55
|
-
- name: Fix code coverage paths
|
|
56
|
-
working-directory: coverage
|
|
57
|
-
run: |
|
|
58
|
-
sed -i 's@'$GITHUB_WORKSPACE'@/github/workspace/@g' .resultset.json
|
|
59
|
-
ruby -rjson -e 'sqube = JSON.load(File.read(".resultset.json"))["RSpec"]["coverage"].transform_values {|lines| lines["lines"]}; total = { "RSpec" => { "coverage" => sqube, "timestamp" => Time.now.to_i }}; puts JSON.dump(total)' > .resultset.sonarqube.json
|
|
60
|
-
|
|
61
|
-
- name: Set VERSION env
|
|
62
|
-
run: echo "VERSION=$(cat lib/splitclient-rb/version.rb | grep VERSION | awk -F "'" '{print $2}')" >> $GITHUB_ENV
|
|
63
|
-
|
|
64
|
-
- name: SonarQube Scan (Push)
|
|
65
|
-
if: matrix.version == '3.2.2' && github.event_name == 'push'
|
|
66
|
-
uses: SonarSource/sonarcloud-github-action@v5.0.0
|
|
67
|
-
env:
|
|
68
|
-
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
|
|
69
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
70
|
-
with:
|
|
71
|
-
projectBaseDir: .
|
|
72
|
-
args: >
|
|
73
|
-
-Dsonar.host.url=${{ secrets.SONARQUBE_HOST }}
|
|
74
|
-
-Dsonar.projectVersion=${{ env.VERSION }}
|
|
75
|
-
|
|
76
|
-
- name: SonarQube Scan (Pull Request)
|
|
77
|
-
if: matrix.version == '3.2.2' && github.event_name == 'pull_request'
|
|
78
|
-
uses: SonarSource/sonarcloud-github-action@v5.0.0
|
|
79
|
-
env:
|
|
80
|
-
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
|
|
81
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
82
|
-
with:
|
|
83
|
-
projectBaseDir: .
|
|
84
|
-
args: >
|
|
85
|
-
-Dsonar.java.source=17
|
|
86
|
-
-Dsonar.host.url=${{ secrets.SONARQUBE_HOST }}
|
|
87
|
-
-Dsonar.projectVersion=${{ env.VERSION }}
|
|
88
|
-
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
|
|
89
|
-
-Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }}
|
|
90
|
-
-Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
name: Update License Year
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
schedule:
|
|
5
|
-
- cron: "0 3 1 1 *" # 03:00 AM on January 1
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: write
|
|
9
|
-
pull-requests: write
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
test:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout
|
|
16
|
-
uses: actions/checkout@v2
|
|
17
|
-
with:
|
|
18
|
-
fetch-depth: 0
|
|
19
|
-
|
|
20
|
-
- name: Set Current year
|
|
21
|
-
run: "echo CURRENT=$(date +%Y) >> $GITHUB_ENV"
|
|
22
|
-
|
|
23
|
-
- name: Set Previous Year
|
|
24
|
-
run: "echo PREVIOUS=$(($CURRENT-1)) >> $GITHUB_ENV"
|
|
25
|
-
|
|
26
|
-
- name: Update LICENSE
|
|
27
|
-
uses: jacobtomlinson/gha-find-replace@v2
|
|
28
|
-
with:
|
|
29
|
-
find: ${{ env.PREVIOUS }}
|
|
30
|
-
replace: ${{ env.CURRENT }}
|
|
31
|
-
include: "LICENSE"
|
|
32
|
-
regex: false
|
|
33
|
-
|
|
34
|
-
- name: Commit files
|
|
35
|
-
run: |
|
|
36
|
-
git config user.name 'github-actions[bot]'
|
|
37
|
-
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
|
38
|
-
git commit -m "Updated License Year" -a
|
|
39
|
-
|
|
40
|
-
- name: Create Pull Request
|
|
41
|
-
uses: peter-evans/create-pull-request@v3
|
|
42
|
-
with:
|
|
43
|
-
token: ${{ secrets.GITHUB_TOKEN }}
|
|
44
|
-
title: Update License Year
|
|
45
|
-
branch: update-license
|