share-data-watcher 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1a9dd6ba2eb14954eff10b7dbf83658af09a1b3e4c7a6c0ca80ea955b3bdb26d
4
+ data.tar.gz: 7a6982813617d097edaa1ca5e0369e95ee6181ac45e2d1f8d8f9488f6d630cd6
5
+ SHA512:
6
+ metadata.gz: 5ad3ab8e0b3a5928f751123c93c1528845ab64ffde519d30b714bc92494583ca606e97dee28ff1533359e091ff7656c4541686e6a4754220e5b737a3b4271cae
7
+ data.tar.gz: 3f81af461d2a95999b2c0340d5e8b7b16f522d90edfed427bbd7417b474fbfd286a10134396d8385697c8cdae9401176856b6fbca0b3bf15b4ac7ef08e4af9a1
data/.gitignore ADDED
@@ -0,0 +1,36 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+
20
+ # Vim
21
+ *.swp
22
+ *.swo
23
+
24
+ # ctags
25
+ tags
26
+
27
+ #Mac
28
+ .DS_Store
29
+ tmp/
30
+
31
+ .idea
32
+ reports/
33
+ target/
34
+ log
35
+ migrations/*.db
36
+ vendor
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format Fuubar
2
+ --color
3
+ --format html
4
+ --out reports/rspec/result.html
data/.rubocop.yml ADDED
@@ -0,0 +1,18 @@
1
+ # Common configuration.
2
+
3
+ require:
4
+ - rubocop-performance
5
+
6
+ AllCops:
7
+ TargetRubyVersion: 2.5
8
+ Exclude:
9
+ - target/**/*
10
+
11
+
12
+ #################### Layout ###########################
13
+ Layout/LineLength:
14
+ Max: 120
15
+
16
+ #################### Metrics ###########################
17
+ Metrics/MethodLength:
18
+ Max: 20
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.5
data/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file
3
+
4
+
5
+ ***
6
+
7
+ ## [0.2.1]
8
+
9
+ ### Changed
10
+ * after_change hook pass builded object to proc hook
11
+
12
+
13
+ ***
14
+
15
+ ## [0.2.0]
16
+
17
+ ### Added
18
+ * Watch Type support after_change hook
19
+
20
+
21
+ ***
22
+
23
+ ## [0.1.1]
24
+
25
+ ### Added
26
+ * Regx Key support "-" And "_"
27
+
28
+
29
+ ***
30
+
31
+ ## [0.1.0]
32
+ # First Release
33
+
34
+ ### Added
35
+
36
+ ### Changed
37
+
38
+ ### Deleted
39
+
40
+ ***
data/Gemfile ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'http://rubygems.org'
4
+
5
+ ruby '~> 2.5'
6
+
7
+ gemspec
8
+
9
+ group :test do
10
+ gem 'as-duration'
11
+ gem 'faker'
12
+ gem 'fuubar'
13
+ gem 'rspec'
14
+ gem 'rspec-nc'
15
+ gem 'simplecov'
16
+ gem 'ulid'
17
+ end
18
+
19
+ group :development do
20
+ gem 'rubocop', require: false
21
+ gem 'rubocop-performance', require: false
22
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,124 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ share-data-watcher (0.2.1)
5
+ bundler (>= 1.16.1)
6
+ dry-struct (>= 1.2.0)
7
+ etcdv3 (~> 0.10.0)
8
+ multi_json (>= 1.14.1)
9
+ oj (>= 3.9.2)
10
+
11
+ GEM
12
+ remote: http://rubygems.org/
13
+ specs:
14
+ as-duration (0.1.1)
15
+ ast (2.4.0)
16
+ concurrent-ruby (1.1.6)
17
+ diff-lcs (1.3)
18
+ docile (1.3.2)
19
+ dry-configurable (0.11.1)
20
+ concurrent-ruby (~> 1.0)
21
+ dry-core (~> 0.4, >= 0.4.7)
22
+ dry-equalizer (~> 0.2)
23
+ dry-container (0.7.2)
24
+ concurrent-ruby (~> 1.0)
25
+ dry-configurable (~> 0.1, >= 0.1.3)
26
+ dry-core (0.4.9)
27
+ concurrent-ruby (~> 1.0)
28
+ dry-equalizer (0.3.0)
29
+ dry-inflector (0.2.0)
30
+ dry-logic (1.0.6)
31
+ concurrent-ruby (~> 1.0)
32
+ dry-core (~> 0.2)
33
+ dry-equalizer (~> 0.2)
34
+ dry-struct (1.3.0)
35
+ dry-core (~> 0.4, >= 0.4.4)
36
+ dry-equalizer (~> 0.3)
37
+ dry-types (~> 1.3)
38
+ ice_nine (~> 0.11)
39
+ dry-types (1.3.1)
40
+ concurrent-ruby (~> 1.0)
41
+ dry-container (~> 0.3)
42
+ dry-core (~> 0.4, >= 0.4.4)
43
+ dry-equalizer (~> 0.3)
44
+ dry-inflector (~> 0.1, >= 0.1.2)
45
+ dry-logic (~> 1.0, >= 1.0.2)
46
+ etcdv3 (0.10.2)
47
+ grpc (~> 1.17)
48
+ faker (2.10.2)
49
+ i18n (>= 1.6, < 2)
50
+ fuubar (2.5.0)
51
+ rspec-core (~> 3.0)
52
+ ruby-progressbar (~> 1.4)
53
+ google-protobuf (3.11.4)
54
+ googleapis-common-protos-types (1.0.4)
55
+ google-protobuf (~> 3.0)
56
+ grpc (1.27.0)
57
+ google-protobuf (~> 3.11)
58
+ googleapis-common-protos-types (~> 1.0)
59
+ i18n (1.8.2)
60
+ concurrent-ruby (~> 1.0)
61
+ ice_nine (0.11.2)
62
+ jaro_winkler (1.5.4)
63
+ multi_json (1.14.1)
64
+ oj (3.10.2)
65
+ parallel (1.19.1)
66
+ parser (2.7.0.2)
67
+ ast (~> 2.4.0)
68
+ rainbow (3.0.0)
69
+ rexml (3.2.4)
70
+ rspec (3.9.0)
71
+ rspec-core (~> 3.9.0)
72
+ rspec-expectations (~> 3.9.0)
73
+ rspec-mocks (~> 3.9.0)
74
+ rspec-core (3.9.1)
75
+ rspec-support (~> 3.9.1)
76
+ rspec-expectations (3.9.0)
77
+ diff-lcs (>= 1.2.0, < 2.0)
78
+ rspec-support (~> 3.9.0)
79
+ rspec-mocks (3.9.1)
80
+ diff-lcs (>= 1.2.0, < 2.0)
81
+ rspec-support (~> 3.9.0)
82
+ rspec-nc (0.3.0)
83
+ rspec (>= 3)
84
+ terminal-notifier (>= 1.4)
85
+ rspec-support (3.9.2)
86
+ rubocop (0.80.0)
87
+ jaro_winkler (~> 1.5.1)
88
+ parallel (~> 1.10)
89
+ parser (>= 2.7.0.1)
90
+ rainbow (>= 2.2.2, < 4.0)
91
+ rexml
92
+ ruby-progressbar (~> 1.7)
93
+ unicode-display_width (>= 1.4.0, < 1.7)
94
+ rubocop-performance (1.5.2)
95
+ rubocop (>= 0.71.0)
96
+ ruby-progressbar (1.10.1)
97
+ simplecov (0.18.2)
98
+ docile (~> 1.1)
99
+ simplecov-html (~> 0.11)
100
+ simplecov-html (0.12.0)
101
+ terminal-notifier (2.0.0)
102
+ ulid (1.2.0)
103
+ unicode-display_width (1.6.1)
104
+
105
+ PLATFORMS
106
+ ruby
107
+
108
+ DEPENDENCIES
109
+ as-duration
110
+ faker
111
+ fuubar
112
+ rspec
113
+ rspec-nc
114
+ rubocop
115
+ rubocop-performance
116
+ share-data-watcher!
117
+ simplecov
118
+ ulid
119
+
120
+ RUBY VERSION
121
+ ruby 2.5.1p57
122
+
123
+ BUNDLED WITH
124
+ 1.17.2
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # share-data-watcher
2
+
3
+ Share Data Watcher For Game Operation
4
+
5
+ Example:
6
+ - https://bitbucket.org/gamesource/share-data-watcher/src/master/example/
7
+ - https://bitbucket.org/gamesource/share-data-watcher/src/master/spec/functional/sample/
8
+
9
+ Struct Type Please Ref:
10
+ - https://dry-rb.org/gems/dry-struct/1.0/
11
+ - https://dry-rb.org/gems/dry-types/1.2/
@@ -0,0 +1,36 @@
1
+ # This is a sample build configuration for Ruby.
2
+ # Check our guides at https://confluence.atlassian.com/x/8r-5Mw for more examples.
3
+ # Only use spaces to indent your .yml configuration.
4
+ # -----
5
+ # You can specify a custom docker image from Docker Hub as your build environment.
6
+ image: ruby:2.5.5
7
+
8
+ pipelines:
9
+ default:
10
+ - step:
11
+ caches:
12
+ - bundler
13
+ script:
14
+ - bundle install
15
+ - bundle exec rspec -cfd
16
+ services:
17
+ - etcd
18
+
19
+ definitions:
20
+ caches:
21
+ bundler: ./vendor
22
+ services:
23
+ etcd:
24
+ image: quay.io/coreos/etcd:v3.3
25
+ variables:
26
+ ETCD_NAME: 'etcd-1'
27
+ ETCD_HEARTBEAT_INTERVAL: '250'
28
+ ETCD_ELECTION_TIMEOUT: '1250'
29
+ ETCD_INITIAL_CLUSTER_STATE: 'new'
30
+ ETCD_INITIAL_ADVERTISE_PEER_URLS: 'http://0.0.0.0:2380'
31
+ ETCD_INITIAL_CLUSTER: 'etcd-1=http://0.0.0.0:2380'
32
+ ETCD_INITIAL_CLUSTER_TOKEN: 'mys3cr3ttok3n'
33
+ ETCD_LISTEN_PEER_URLS: 'http://0.0.0.0:2380'
34
+ ETCD_LISTEN_CLIENT_URLS: 'http://0.0.0.0:2379'
35
+ ETCD_ADVERTISE_CLIENT_URLS: 'http://0.0.0.0:2379'
36
+ ETCD_AUTO_COMPACTION_RETENTION: 1
data/config.reek ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ UtilityFunction:
3
+ public_methods_only: true
4
+ TooManyStatements:
5
+ exclude:
6
+ - initialize
7
+ max_statements: 10
@@ -0,0 +1,21 @@
1
+ version: '3'
2
+
3
+ services:
4
+ etcd-1:
5
+ image: quay.io/coreos/etcd:v3.3
6
+ container_name: etcd-1
7
+ environment:
8
+ ETCD_NAME: 'etcd-1'
9
+ ETCD_HEARTBEAT_INTERVAL: '250'
10
+ ETCD_ELECTION_TIMEOUT: '1250'
11
+ ETCD_INITIAL_CLUSTER_STATE: 'new'
12
+ ETCD_INITIAL_ADVERTISE_PEER_URLS: 'http://0.0.0.0:2380'
13
+ ETCD_INITIAL_CLUSTER: 'etcd-1=http://0.0.0.0:2380'
14
+ ETCD_INITIAL_CLUSTER_TOKEN: 'mys3cr3ttok3n'
15
+ ETCD_LISTEN_PEER_URLS: 'http://0.0.0.0:2380'
16
+ ETCD_LISTEN_CLIENT_URLS: 'http://0.0.0.0:2379'
17
+ ETCD_ADVERTISE_CLIENT_URLS: 'http://0.0.0.0:2379'
18
+ ETCD_AUTO_COMPACTION_RETENTION: 1
19
+ ports:
20
+ - "2379:2379"
21
+ - "2380:2380"
data/example/sample ADDED
@@ -0,0 +1 @@
1
+ ../spec/functional/sample
data/example/sample.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/share-data-watcher'
4
+ require_relative 'sample/watcher'
5
+ require 'logger'
6
+ require 'ulid'
7
+ require 'json'
8
+
9
+ endpoints = ENV.fetch('ETCD_URL', 'http://0.0.0.0:2379')
10
+ healthcheck_interval = 10..30
11
+ ShareDataWatcher.logger = Logger.new(STDOUT)
12
+
13
+ watcher = Watcher.new(endpoints, healthcheck_interval: healthcheck_interval)
14
+ etcd_conn = Etcdv3.new(endpoints: endpoints, allow_reconnect: false)
15
+
16
+ Thread.new do
17
+ 5.times do
18
+ sleep 3
19
+ value = ULID.generate
20
+ key = 'go.game_sessions.force_leave'
21
+ etcd_conn.put(key, JSON.dump([value]))
22
+ puts "#{Time.now} :: update #{key} value #{value}"
23
+ end
24
+ end
25
+
26
+ watcher.start
27
+
28
+ sleep
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShareDataWatcher
4
+ # Class Methods
5
+ module ClassInterface
6
+ %i[watch fetch].each do |mesh|
7
+ define_method(mesh) do |name, key, **opts|
8
+ create_type(
9
+ type: mesh.to_sym,
10
+ name: name,
11
+ params: [
12
+ key,
13
+ opts
14
+ ]
15
+ )
16
+ end
17
+ end
18
+
19
+ def types
20
+ @types ||= []
21
+ end
22
+
23
+ protected
24
+
25
+ TypeParams = Object::Struct.new(
26
+ :name,
27
+ :type,
28
+ :params,
29
+ keyword_init: true
30
+ )
31
+
32
+ def create_type(name:, type:, params:)
33
+ types << TypeParams.new(
34
+ type: type,
35
+ name: name,
36
+ params: params
37
+ )
38
+ define_method(name) do
39
+ @types[name]
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'etcdv3'
4
+
5
+ class ShareDataWatcher
6
+ # Etcd connection client
7
+ class Connection
8
+ attr_reader :conn
9
+
10
+ DEFAULT_FETCH_TIMEOUT = 1
11
+
12
+ def initialize(endpoints)
13
+ @conn = Etcdv3.new(endpoints: endpoints, allow_reconnect: false)
14
+ end
15
+
16
+ def connecting_endpoint
17
+ conn.conn.connection.endpoint.to_s
18
+ end
19
+
20
+ def healthcheck
21
+ conn.version
22
+ true
23
+ rescue StandardError
24
+ false
25
+ end
26
+
27
+ def watch(key, prefix: true, start_revision: nil)
28
+ opt = {
29
+ timeout: Etcdv3::Watch::INFINITE_FUTURE
30
+ }
31
+ opt[:range_end] = increase_one_bit(key) if prefix
32
+ opt[:start_revision] = start_revision if start_revision
33
+ conn.watch(key, opt) do |events|
34
+ events.each do |event|
35
+ yield(event.type, event.kv)
36
+ end
37
+ end
38
+ end
39
+
40
+ def fetch(key, prefix: false, timeout: nil)
41
+ opt = {}
42
+ opt[:range_end] = increase_one_bit(key) if prefix
43
+ opt[:timeout] = timeout || DEFAULT_FETCH_TIMEOUT
44
+ res = conn.get(key, opt)
45
+ revision = res.header.revision
46
+ kvs = res.kvs
47
+ block_given? ? yield(revision, kvs) : [revision, kvs]
48
+ end
49
+
50
+ protected
51
+
52
+ def increase_one_bit(key)
53
+ str = key.dup
54
+ str[-1] = (str.getbyte(-1) + 1).chr
55
+ str
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShareDataWatcher
4
+ # Instance Methods For Shard Data Watcher
5
+ module InstanceMethods
6
+ attr_reader :endpoints
7
+ attr_reader :healthcheck_interval
8
+
9
+ def initialize(endpoints = nil, **opt)
10
+ @endpoints = endpoints || DEFAULT_ENDPOINT
11
+ @healthcheck_thread = nil
12
+ @healthcheck_interval = opt[:healthcheck_interval] || DEFAULT_HEALTHCHECK_INTERVAL
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def start
17
+ @mutex.synchronize do
18
+ self.class.types.each do |type|
19
+ init_type(type)
20
+ end
21
+ end
22
+
23
+ until valid?; sleep 1; end
24
+ setup_healthcheck
25
+ self
26
+ end
27
+
28
+ def restart
29
+ shutdown
30
+ start
31
+ setup_healthcheck
32
+ self
33
+ end
34
+
35
+ def shutdown
36
+ types.each_value(&:shutdown)
37
+ types.clear
38
+ @healthcheck_thread.exit
39
+ self
40
+ end
41
+
42
+ def valid?
43
+ types.each do |_, value|
44
+ return false unless value.valid?
45
+ end
46
+ true
47
+ end
48
+
49
+ def to_h
50
+ types.each_with_object({}) do |(key, value), result|
51
+ result[key] = value.to_h
52
+ end
53
+ end
54
+
55
+ alias to_hash to_h
56
+
57
+ protected
58
+
59
+ def init_type(type)
60
+ klass = Type.const_get(str_camelize(type.type.to_s))
61
+ types[type.name] = klass.new(*compose_params(type.params))
62
+ end
63
+
64
+ def setup_healthcheck
65
+ @healthcheck_thread = Thread.new do
66
+ loop do
67
+ sleep rand(healthcheck_interval)
68
+ types.each do |key, value|
69
+ if value.healthcheck
70
+ debug("#{key} alive!")
71
+ else
72
+ debug("#{key} dead! restart now")
73
+ @mutex.synchronize { obj.restart }
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def compose_params(params)
81
+ params.dup.unshift(endpoints)
82
+ end
83
+
84
+ def types
85
+ @types ||= {}
86
+ end
87
+
88
+ def str_camelize(str)
89
+ str.dup.split('_').map(&:capitalize).join
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShareDataWatcher
4
+ # Logger Module
5
+ module Loggable
6
+ def info(string)
7
+ ShareDataWatcher.logger&.info(string)
8
+ end
9
+
10
+ def debug(string)
11
+ ShareDataWatcher.logger&.debug(string)
12
+ end
13
+
14
+ def warn(string)
15
+ ShareDataWatcher.logger&.warn(string)
16
+ end
17
+
18
+ def error(err)
19
+ logger = ShareDataWatcher.logger
20
+ case err
21
+ when StandardError
22
+ logger&.error(
23
+ err: "#{err.class}:#{err.message}",
24
+ backtrace: err.backtrace.join("\n")
25
+ )
26
+ else
27
+ logger&.error err
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShareDataWatcher
4
+ module Struct
5
+ # Base Type
6
+ class Base < Dry::Struct
7
+ include Dry.Types()
8
+
9
+ class << self
10
+ def build(_data)
11
+ raise NotImplementedError
12
+ end
13
+ end
14
+ end
15
+
16
+ # String Type
17
+ class String < Base
18
+ class << self
19
+ def build(data)
20
+ data.to_s
21
+ end
22
+ end
23
+ end
24
+
25
+ # Hash Type
26
+ class Hash < Base
27
+ class << self
28
+ def build(data)
29
+ MultiJson.load(
30
+ data,
31
+ symbolize_keys: true
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShareDataWatcher
4
+ module Type
5
+ # Base Type
6
+ class Base
7
+ include ShareDataWatcher::Loggable
8
+
9
+ extend Forwardable
10
+ def_delegators :connection, :connecting_endpoint, :healthcheck
11
+
12
+ attr_reader :key
13
+
14
+ def initialize(endpoints, key, opt = {})
15
+ @key = key
16
+ @struct = determine_struct_klass(opt[:struct])
17
+ @extension = opt[:extension]
18
+ @regex = extension && Regexp.new("^#{key}#{extension.gsub(/[.]/, '[.]').gsub(/%{(\w+)}/, '([a-zA-Z0-9\-\_]+)')}$")
19
+ @connection = Connection.new(endpoints)
20
+ reset
21
+ on_init
22
+ end
23
+
24
+ def get(**params)
25
+ key = fetch_key(params)
26
+ values[key.to_sym]
27
+ end
28
+
29
+ def valid?
30
+ @valid
31
+ end
32
+
33
+ def shutdown
34
+ reset
35
+ end
36
+
37
+ def restart
38
+ shutdown
39
+ on_init
40
+ end
41
+
42
+ def to_h
43
+ values.each_with_object({}) do |(key, value), result|
44
+ result[key] = value.respond_to?(:to_h) ? value.to_h : value
45
+ end
46
+ end
47
+
48
+ alias to_hash to_h
49
+
50
+ protected
51
+
52
+ def determine_struct_klass(klass)
53
+ return Struct::String if klass.nil?
54
+ raise ArgumentError, 'struct class must inherited from Struct::Base' unless klass <= Struct::Base
55
+
56
+ klass
57
+ end
58
+
59
+ def on_init; end
60
+
61
+ attr_reader :values
62
+ attr_reader :struct
63
+ attr_reader :connection
64
+ attr_reader :extension
65
+ attr_reader :regex
66
+
67
+ def fetch_key(**params)
68
+ prefix? ? format("#{key}#{extension}", params) : key
69
+ end
70
+
71
+ def prefix?
72
+ !extension.nil?
73
+ end
74
+
75
+ def put(key, value)
76
+ return unless regex_check?(key)
77
+
78
+ info("ShareDataWatcher #{key} update: #{value}")
79
+ values[key.to_sym] = struct.build(value)
80
+ rescue StandardError => e
81
+ delete(key)
82
+ error e
83
+ error "remove #{key} on ShareDataWatcher #{self.key}"
84
+ end
85
+
86
+ def regex_check?(key)
87
+ return true unless regex && regex !~ key
88
+
89
+ info "#{key} is not match #{regex}."
90
+ false
91
+ end
92
+
93
+ def delete(key)
94
+ info("ShareDataWatcher delete: #{key}")
95
+ values.delete(key.to_sym)
96
+ end
97
+
98
+ def reset
99
+ @valid = false
100
+ @values = {}
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShareDataWatcher
4
+ module Type
5
+ # Fetch Type
6
+ # For cache share watch
7
+ class Fetch < Base
8
+ attr_reader :timeout
9
+
10
+ # opt[:struct]
11
+ # opt[:extension]
12
+ # opt[:timeout]
13
+ def initialize(endpoints, key, opt = {})
14
+ @timeout = opt[:timeout]
15
+ super
16
+ end
17
+
18
+ def get(**params)
19
+ key = fetch_key(params)
20
+ values[key.to_sym] || fetch(key)
21
+ end
22
+
23
+ protected
24
+
25
+ def fetch(key)
26
+ _revision, kvs = connection.fetch(key, prefix: false, timeout: timeout)
27
+ kv = kvs.first
28
+ return nil unless kv
29
+
30
+ put(kv.key, kv.value)
31
+ values[key.to_sym]
32
+ end
33
+
34
+ def on_init
35
+ @valid = true
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShareDataWatcher
4
+ module Type
5
+ # Watch Type
6
+ # Watch key changes
7
+ class Watch < Base
8
+ attr_reader :start_revision
9
+ attr_reader :retry_interval
10
+ attr_reader :after_change
11
+
12
+ DEFAULT_RETRY_INTERVAL = 30
13
+
14
+ # opt[:struct]
15
+ # opt[:retry_interval]
16
+ # opt[:extension]
17
+ def initialize(endpoints, key, opt = {})
18
+ @retry_interval = opt[:retry_interval] || DEFAULT_RETRY_INTERVAL
19
+ @after_change = opt[:after_change] || nil
20
+
21
+ if after_change && !after_change.is_a?(Proc)
22
+ raise ArgumentError, 'after_change must be proc'
23
+ end
24
+
25
+ @thread = nil
26
+ super
27
+ end
28
+
29
+ def shutdown
30
+ @thread.exit
31
+ super
32
+ end
33
+
34
+ protected
35
+
36
+ def reset
37
+ @start_revision = -1
38
+ super
39
+ end
40
+
41
+ def on_init
42
+ @thread = Thread.new do
43
+ setup
44
+ end
45
+ end
46
+
47
+ def init_values
48
+ @start_revision, kvs = connection.fetch(key, prefix: prefix?)
49
+ info("ShareDataWatcher init #{key}, kvs: #{kvs}, start_revision: #{start_revision}")
50
+
51
+ kvs.each do |kv|
52
+ put(kv.key, kv.value)
53
+ end
54
+ @valid = true
55
+ end
56
+
57
+ def setup_watcher
58
+ connection.watch(key, prefix: prefix?, start_revision: start_revision) do |type, kv|
59
+ case type
60
+ when :PUT
61
+ put(kv.key, kv.value)
62
+ after_change&.call(type, kv.key, values[kv.key.to_sym])
63
+ when :DELETE
64
+ delete(kv.key)
65
+ after_change&.call(type, kv.key, nil)
66
+ end
67
+ end
68
+ end
69
+
70
+ def setup
71
+ init_values
72
+ setup_watcher
73
+ rescue StandardError => e
74
+ reset
75
+ error(e)
76
+ error("Reset ShareDataWatcher #{key}")
77
+ sleep retry_interval
78
+ retry
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type/base'
4
+
5
+ # load mysql repository module stuffs in /type folder
6
+ Dir["#{File.dirname(__FILE__)}/type/**.rb"]
7
+ .each { |file| require_relative file }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ShareDataWatcher
4
+ VERSION = '0.2.1'
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'etcdv3'
4
+ require 'multi_json'
5
+ require 'oj'
6
+ require 'dry-struct'
7
+
8
+ # Watch etcd value for IAS
9
+ class ShareDataWatcher
10
+ require_relative 'share-data-watcher/loggable'
11
+
12
+ Dir["#{File.dirname(__FILE__)}/share-data-watcher/**.rb"]
13
+ .each { |file| require_relative file }
14
+
15
+ include Loggable
16
+ include InstanceMethods
17
+ extend ClassInterface
18
+
19
+ DEFAULT_ENDPOINT = 'http://localhost:2379'
20
+ DEFAULT_HEALTHCHECK_INTERVAL = (30..90).freeze
21
+
22
+ class << self
23
+ attr_accessor :logger
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('lib/share-data-watcher/version', __dir__)
4
+ # $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'share-data-watcher'
8
+ spec.version = ShareDataWatcher::VERSION
9
+ spec.authors = ['Cheok Meng Chan']
10
+ spec.email = ['cheokmeng.chan@gamesourcecloud.com']
11
+ spec.required_ruby_version = '>= 2.5'
12
+
13
+ spec.summary = 'Game Operation Data Watcher'
14
+ spec.description = 'Game Operation Data Watcher For IAS'
15
+ spec.homepage = 'https://bitbucket.org/gamesource/go-data-watcher'
16
+
17
+ if spec.respond_to?(:metadata)
18
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
19
+ else
20
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
21
+ end
22
+ spec.require_paths = ['lib']
23
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ spec.add_dependency 'bundler', '>= 1.16.1'
25
+ spec.add_dependency 'dry-struct', '>= 1.2.0'
26
+ spec.add_dependency 'etcdv3', '~> 0.10.0'
27
+ spec.add_dependency 'multi_json', '>= 1.14.1'
28
+ spec.add_dependency 'oj', '>= 3.9.2'
29
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: share-data-watcher
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Cheok Meng Chan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-06-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.16.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.16.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-struct
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: etcdv3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.10.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.10.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: multi_json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 1.14.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 1.14.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: oj
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 3.9.2
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 3.9.2
83
+ description: Game Operation Data Watcher For IAS
84
+ email:
85
+ - cheokmeng.chan@gamesourcecloud.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".rubocop.yml"
93
+ - ".ruby-version"
94
+ - CHANGELOG.md
95
+ - Gemfile
96
+ - Gemfile.lock
97
+ - README.md
98
+ - bitbucket-pipelines.yml
99
+ - config.reek
100
+ - docker-compose.yml
101
+ - example/sample
102
+ - example/sample.rb
103
+ - lib/share-data-watcher.rb
104
+ - lib/share-data-watcher/class_interface.rb
105
+ - lib/share-data-watcher/connection.rb
106
+ - lib/share-data-watcher/instance_methods.rb
107
+ - lib/share-data-watcher/loggable.rb
108
+ - lib/share-data-watcher/struct.rb
109
+ - lib/share-data-watcher/type.rb
110
+ - lib/share-data-watcher/type/base.rb
111
+ - lib/share-data-watcher/type/fetch.rb
112
+ - lib/share-data-watcher/type/watch.rb
113
+ - lib/share-data-watcher/version.rb
114
+ - share-data-watch.gemspec
115
+ homepage: https://bitbucket.org/gamesource/go-data-watcher
116
+ licenses: []
117
+ metadata:
118
+ allowed_push_host: https://rubygems.org
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '2.5'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.0.3.1
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Game Operation Data Watcher
138
+ test_files: []