thinkingdata-ruby 2.0.1 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba7de0dac467fe8c90747854c7701e0af035463b9b48b4708308e4c28d242c90
4
- data.tar.gz: fa29bd005cbe4b32cceecd85d726e79b7b0e0fb4e51346b649f25d7b44affcca
3
+ metadata.gz: 5faa8253295effbc9ff765bc22bca617bd822ee9d4c7de19bbbce266413f3c1f
4
+ data.tar.gz: 4872918901d705fc9ae980279dfdae1a15b2e266a7372df3a4ae9e1d8c9b676d
5
5
  SHA512:
6
- metadata.gz: e73fadf565e9143fd67ad1c99e1472ee027d7eda707a4b2452705b620d05d1f223b71e99d61b84c0bbb353792f2f1f62212c7d2c04e670c736ee5e94db74291e
7
- data.tar.gz: 39fce15278418ba6d92dface6276acee8760953954ff5521fb115f91c81bf83807e063e7caf6a59db0fd8e388407f74a7a0748d02b99086d3de91c8f2b0ffd31
6
+ metadata.gz: 4aba5b203c92631de33b27362947fed059eb6b9ff8d2b1946e2a196d99fcf9b832087f528df72c6a37e5f2424bc2a59eaf556493182b83bf7542761ea5053c93
7
+ data.tar.gz: 975676199eb20cac647afafff3982d220fd11814c84f538819cb58741c9ab3e27493dba586110a3d02c2fd73843815df5ef74baac8a85b16a4aac7abe4456676
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ### v2.0.2
2
+ **Date:** 2026/06/05
3
+
4
+ **Notes:**
5
+
6
+ * Enhance the stability of the SDK
7
+
1
8
  ### v2.0.1
2
9
  **Date:** 2026/05/13
3
10
 
@@ -48,9 +48,7 @@ module ThinkingData
48
48
  LIB_PROPERTIES = {
49
49
  '#lib' => 'ruby',
50
50
  '#lib_version' => ThinkingData::VERSION,
51
- }
52
-
53
- @dynamic_block = nil
51
+ }.freeze
54
52
 
55
53
  ##
56
54
  # Init function
@@ -60,7 +58,7 @@ module ThinkingData
60
58
  def initialize(consumer, error_handler = nil, uuid: false)
61
59
  @error_handler = error_handler || TDErrorHandler.new
62
60
  @consumer = consumer
63
- @super_properties = {}
61
+ @super_properties = {}.freeze
64
62
  @uuid_enable = uuid
65
63
  @mutex = Mutex.new
66
64
  TDLog.info("SDK init success.")
@@ -74,13 +72,11 @@ module ThinkingData
74
72
  @error_handler.handle(IllegalParameterError.new("Invalid super properties"))
75
73
  return false
76
74
  end
75
+ formatted = {}
77
76
  properties.each do |k, v|
78
- if v.is_a?(Time)
79
- @super_properties[k] = _format_time(v)
80
- else
81
- @super_properties[k] = v
82
- end
77
+ formatted[k] = v.is_a?(Time) ? _format_time(v) : v
83
78
  end
79
+ @super_properties = @super_properties.merge(formatted).freeze
84
80
  end
85
81
  end
86
82
 
@@ -88,7 +84,7 @@ module ThinkingData
88
84
  # Clear super properties
89
85
  def clear_super_properties
90
86
  @mutex.synchronize do
91
- @super_properties = {}
87
+ @super_properties = {}.freeze
92
88
  end
93
89
  end
94
90
 
@@ -341,7 +337,7 @@ module ThinkingData
341
337
  # Report data immediately
342
338
  def flush
343
339
  TDLog.info("SDK flush data.")
344
- return true unless defined? @consumer.flush
340
+ return true unless @consumer.respond_to?(:flush)
345
341
  ret = true
346
342
  begin
347
343
  @consumer.flush
@@ -355,7 +351,7 @@ module ThinkingData
355
351
  ##
356
352
  # Close and exit sdk
357
353
  def close
358
- return true unless defined? @consumer.close
354
+ return true unless @consumer.respond_to?(:close)
359
355
  ret = true
360
356
  # Consumer 自身已有锁保护,无需在此加锁
361
357
  begin
@@ -375,12 +371,17 @@ module ThinkingData
375
371
  def _internal_track(type, properties: {}, event_name: nil, event_id:nil, account_id: nil, distinct_id: nil, ip: nil,first_check_id: nil, time: nil)
376
372
  ret = true
377
373
 
378
- # 只在访问共享状态时加锁
374
+ # Snapshot @dynamic_block under the mutex for memory visibility;
375
+ # @super_properties is a frozen hash replaced atomically — safe lock-free read.
376
+ # The dynamic block is called OUTSIDE the mutex to prevent deadlocks
377
+ # (if the block calls back into TDAnalytics) and throughput collapse.
379
378
  if type == :track || type == :track_update || type == :track_overwrite
380
- @mutex.synchronize do
381
- dynamic_properties = @dynamic_block.respond_to?(:call) ? @dynamic_block.call : {}
382
- properties = LIB_PROPERTIES.merge(@super_properties).merge(dynamic_properties).merge(properties)
383
- end
379
+ block = @mutex.synchronize { @dynamic_block }
380
+ dynamic_properties = block.respond_to?(:call) ? block.call : {}
381
+ properties = LIB_PROPERTIES.merge(@super_properties).merge(dynamic_properties).merge(properties)
382
+ else
383
+ # user_* methods: dup to avoid mutating the caller's hash
384
+ properties = properties.dup
384
385
  end
385
386
 
386
387
  data = {
@@ -390,6 +391,8 @@ module ThinkingData
390
391
  properties.each do |k, v|
391
392
  if v.is_a?(Time)
392
393
  properties[k] = _format_time(v)
394
+ elsif v.is_a?(Array)
395
+ properties[k] = v.map { |e| e.is_a?(Time) ? _format_time(e) : e }
393
396
  end
394
397
  end
395
398
 
@@ -466,13 +469,6 @@ module ThinkingData
466
469
  if type == :user_add
467
470
  raise IllegalParameterError.new("Property value for user add must be numbers") unless v.is_a?(Integer) || v.is_a?(Float)
468
471
  end
469
- if v.is_a?(Array)
470
- v.each_index do |i|
471
- if v[i].is_a?(Time)
472
- v[i] = _format_time(v[i])
473
- end
474
- end
475
- end
476
472
  end
477
473
  true
478
474
  end
@@ -21,6 +21,7 @@ module ThinkingData
21
21
  @max_length = [max_buffer_length, MAX_LENGTH].min
22
22
  @buffers = []
23
23
  @mutex = Mutex.new
24
+ @owner_pid = Process.pid
24
25
  TDLog.info("TDBatchConsumer init success. ServerUrl: #{server_url}, appId: #{app_id}")
25
26
  end
26
27
 
@@ -42,6 +43,7 @@ module ThinkingData
42
43
  def add(message)
43
44
  TDLog.info("Enqueue data to buffer. buffer size: #{@buffers.length}, data: #{message}")
44
45
  need_flush = false
46
+ _reset_after_fork_if_needed
45
47
  @mutex.synchronize do
46
48
  @buffers << message
47
49
  need_flush = @buffers.length >= @max_length
@@ -50,11 +52,13 @@ module ThinkingData
50
52
  end
51
53
 
52
54
  def close
55
+ _reset_after_fork_if_needed
53
56
  flush
54
57
  TDLog.info("TDBatchConsumer close.")
55
58
  end
56
59
 
57
60
  def flush
61
+ _reset_after_fork_if_needed
58
62
  TDLog.info("TDBatchConsumer flush data.")
59
63
  data_to_send = nil
60
64
  @mutex.synchronize do
@@ -64,8 +68,9 @@ module ThinkingData
64
68
 
65
69
  return if data_to_send.empty?
66
70
 
67
- begin
68
- data_to_send.each_slice(@max_length) do |chunk|
71
+ chunks = data_to_send.each_slice(@max_length).to_a
72
+ chunks.each_with_index do |chunk, idx|
73
+ begin
69
74
  if @compress
70
75
  wio = StringIO.new("w")
71
76
  gzip_io = Zlib::GzipWriter.new(wio)
@@ -81,7 +86,7 @@ module ThinkingData
81
86
  'compress' => compress_type,
82
87
  'TE-Integration-Type'=>'Ruby',
83
88
  'TE-Integration-Version'=>ThinkingData::VERSION,
84
- 'TE-Integration-Count'=>data_to_send.count,
89
+ 'TE-Integration-Count'=>chunk.count,
85
90
  'TA_Integration-Extra'=>'batch'}
86
91
  request = CaseSensitivePost.new(@server_uri.request_uri, headers)
87
92
  request.body = data
@@ -106,13 +111,25 @@ module ThinkingData
106
111
  if result['code'] != 0
107
112
  raise ServerError.new("Could not write to TE, server responded with #{response_code} returning: '#{response_body}'")
108
113
  end
114
+ rescue => e
115
+ # Re-enqueue the failed chunk AND every not-yet-sent chunk to the back,
116
+ # so that raising here never drops data still pending in this flush.
117
+ # New data already in @buffers stays ahead, failed data retries next flush.
118
+ remaining = chunks[idx..-1].flatten(1)
119
+ @mutex.synchronize { @buffers = @buffers + remaining }
120
+ raise e
109
121
  end
110
- rescue
111
- raise
112
122
  end
113
123
  end
114
124
 
115
125
  private
126
+ def _reset_after_fork_if_needed
127
+ return if @owner_pid == Process.pid
128
+ @owner_pid = Process.pid
129
+ @mutex = Mutex.new
130
+ @buffers = []
131
+ end
132
+
116
133
  def _request(uri, request)
117
134
  client = Net::HTTP.new(uri.host, uri.port)
118
135
  client.use_ssl = uri.scheme === 'https' ? true : false
@@ -1,4 +1,5 @@
1
1
  require 'logger'
2
+ require 'fileutils'
2
3
  require 'thinkingdata-ruby/td_errors'
3
4
 
4
5
  module ThinkingData
@@ -43,11 +44,13 @@ module ThinkingData
43
44
  @log_path = log_path
44
45
  @full_prefix = "#{log_path}/#{prefix}"
45
46
  @mutex = Mutex.new
47
+ @owner_pid = Process.pid
46
48
  TDLog.info("TDLoggerConsumer init success. LogPath: #{log_path}")
47
49
  _reset
48
50
  end
49
51
 
50
52
  def add(msg)
53
+ _reset_after_fork_if_needed
51
54
  @mutex.synchronize do
52
55
  unless Time.now.strftime(@suffix_mode) == @current_suffix
53
56
  @logger.close
@@ -66,6 +69,7 @@ module ThinkingData
66
69
  end
67
70
 
68
71
  def close
72
+ _reset_after_fork_if_needed
69
73
  @mutex.synchronize do
70
74
  @logger.close
71
75
  end
@@ -75,7 +79,7 @@ module ThinkingData
75
79
  private
76
80
 
77
81
  def _reset
78
- Dir::mkdir(@log_path) unless Dir::exist?(@log_path)
82
+ FileUtils.mkdir_p(@log_path)
79
83
  @logger = HeadlessLogger.new("#{@full_prefix}.#{@current_suffix}")
80
84
  @logger.level = HeadlessLogger::INFO
81
85
  @logger.formatter = proc do |severity, datetime, progname, msg|
@@ -83,5 +87,13 @@ module ThinkingData
83
87
  end
84
88
  end
85
89
 
90
+ def _reset_after_fork_if_needed
91
+ return if @owner_pid == Process.pid
92
+ @owner_pid = Process.pid
93
+ @mutex = Mutex.new
94
+ @logger = nil # Don't close — FD is shared with parent
95
+ _reset
96
+ end
97
+
86
98
  end
87
99
  end
@@ -1,3 +1,3 @@
1
1
  module ThinkingData
2
- VERSION = '2.0.1'
2
+ VERSION = '2.0.2'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thinkingdata-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ThinkingData
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-14 00:00:00.000000000 Z
11
+ date: 2026-06-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: The official ThinkingData Analytics API for ruby
14
14
  email: sdk@thinkingdata.cn