betterplace-bi 0.7.0

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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.semaphore/semaphore.yml +26 -0
  4. data/.tool-versions +2 -0
  5. data/.utilsrc +26 -0
  6. data/Gemfile +5 -0
  7. data/README.md +76 -0
  8. data/Rakefile +37 -0
  9. data/VERSION +1 -0
  10. data/betterplace-bi.gemspec +39 -0
  11. data/lib/betterplace-bi.rb +1 -0
  12. data/lib/bi/ab_test_helper.rb +81 -0
  13. data/lib/bi/api.rb +115 -0
  14. data/lib/bi/commands/base.rb +11 -0
  15. data/lib/bi/commands/collector.rb +32 -0
  16. data/lib/bi/commands/connection.rb +25 -0
  17. data/lib/bi/commands/delete.rb +33 -0
  18. data/lib/bi/commands/serializer.rb +40 -0
  19. data/lib/bi/commands/update.rb +29 -0
  20. data/lib/bi/commands.rb +10 -0
  21. data/lib/bi/commands_job.rb +24 -0
  22. data/lib/bi/event.rb +52 -0
  23. data/lib/bi/planning_value_parser.rb +100 -0
  24. data/lib/bi/planning_value_validations.rb +15 -0
  25. data/lib/bi/railtie.rb +13 -0
  26. data/lib/bi/request_analyzer.rb +64 -0
  27. data/lib/bi/session_id.rb +11 -0
  28. data/lib/bi/shared_value.rb +102 -0
  29. data/lib/bi/tracking.rb +28 -0
  30. data/lib/bi/type_generator.rb +79 -0
  31. data/lib/bi/update_error.rb +4 -0
  32. data/lib/bi/updater.rb +61 -0
  33. data/lib/bi/version.rb +8 -0
  34. data/lib/bi.rb +27 -0
  35. data/lib/tasks/bime.rake +55 -0
  36. data/spec/bi/ab_test_helper_spec.rb +145 -0
  37. data/spec/bi/commands/collector_spec.rb +26 -0
  38. data/spec/bi/commands/connection_spec.rb +15 -0
  39. data/spec/bi/commands_job_spec.rb +24 -0
  40. data/spec/bi/commands_spec.rb +46 -0
  41. data/spec/bi/event_spec.rb +77 -0
  42. data/spec/bi/planning_value_parser_spec.rb +94 -0
  43. data/spec/bi/request_analyzer_spec.rb +75 -0
  44. data/spec/bi/shared_value_spec.rb +47 -0
  45. data/spec/bi/tracking_spec.rb +37 -0
  46. data/spec/bi/type_generator_spec.rb +44 -0
  47. data/spec/bi/updater_spec.rb +61 -0
  48. data/spec/bime_dir/.keep +0 -0
  49. data/spec/config/bi.yml +18 -0
  50. data/spec/spec_helper.rb +31 -0
  51. data/spec/support/models.rb +106 -0
  52. metadata +331 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 119422d8d08ceabf4379d677d556c4dc2b6bcb32f0ae4f7b6304164bc7fabd59
4
+ data.tar.gz: 2cccf0803769aec1947716a60ea2d1dec45074785347b6a28058a92c99f7d3e9
5
+ SHA512:
6
+ metadata.gz: 34c3d5aa9e9f363011bdf77fa53e5b4bfaaac9728a29f916d14e0b1b7769e0364c03e8c46ee1572e50316cfe09fe5e9ff18e9375fe7fb9de3078a3f20fee2373
7
+ data.tar.gz: 3b96febf3b0a6aa22ef134980e4a574603ea8d2020661af16abac9016a6354c84dc9d65a216664cfcc576e8fb790caf68feeb8e58a79e0af4cd5d31cde68430a
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ .*.sw[pon]
2
+ .AppleDouble
3
+ .DS_Store
4
+ .byebug_history
5
+ .rvmrc
6
+ Gemfile.lock
7
+ coverage
8
+ errors.lst
9
+ pkg
10
+ tags
@@ -0,0 +1,26 @@
1
+ version: v1.0
2
+ name: Betterplace-BI pipeline
3
+ agent:
4
+ machine:
5
+ type: e1-standard-2
6
+ os_image: ubuntu2004
7
+
8
+ blocks:
9
+ - name: "Unit Tests"
10
+ task:
11
+ env_vars:
12
+ # Matches the configuration used in sem-service
13
+ - name: RAILS_ENV
14
+ value: test
15
+ jobs:
16
+ - name: RSpec
17
+ commands:
18
+ - checkout
19
+
20
+ # Setup ruby
21
+ - sem-version ruby $(awk '/^ruby/ { print $2 }' .tool-versions)
22
+
23
+ # Setup gems
24
+ - bundle config set path 'vendor/bundle'
25
+ - bundle install
26
+ - bundle exec rake spec
data/.tool-versions ADDED
@@ -0,0 +1,2 @@
1
+ ruby 3.2.0
2
+ bundler 2.2.32
data/.utilsrc ADDED
@@ -0,0 +1,26 @@
1
+ # vim: set ft=ruby:
2
+
3
+ search do
4
+ prune_dirs /\A(\.svn|\.git|CVS|tmp|tags|coverage|pkg)\z/
5
+ skip_files /(\A\.|\.sw[pon]\z|\.(log|fnm|jpg|jpeg|png|pdf|svg)\z|tags|~\z)/i
6
+ end
7
+
8
+ discover do
9
+ prune_dirs /\A(\.svn|\.git|CVS|tmp|tags|coverage|pkg)\z/
10
+ skip_files /(\A\.|\.sw[pon]\z|\.log\z|~\z)/
11
+ binary false
12
+ end
13
+
14
+ strip_spaces do
15
+ prune_dirs /\A(\..*|CVS|pkg)\z/
16
+ skip_files /(\A\.|\.sw[pon]\z|\.log\z|~\z)/
17
+ end
18
+
19
+ probe do
20
+ test_framework :rspec
21
+ #include_dirs 'features'
22
+ end
23
+
24
+ ssh_tunnel do
25
+ terminal_multiplexer :tmux
26
+ end
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # vim: set filetype=ruby et sw=2 ts=2:
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Betterplace-BI
2
+
3
+ ## Description
4
+
5
+ This library transfers BI data from an application to a BIME-server.
6
+
7
+ ## Configuration
8
+
9
+ Via a `config/bi.yml` file in YAML like so:
10
+
11
+ ```
12
+ ---
13
+ production:
14
+ update: yes
15
+ auth:
16
+ username: bi
17
+ password: secret
18
+ clear: http://localhost:1234/api/v1/all/:id
19
+ endpoints:
20
+ BI::TestModelValue:
21
+ POST: http://localhost:1234/api/v1/tests
22
+ DELETE: http://localhost:1234/api/v1/test/:id
23
+ bime_dir:
24
+ <%= Pathname.pwd.join('../bime') %>
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ For every Rails model `TestModel` define a class named
30
+ `BI::TestModelValue` like this:
31
+
32
+ ```
33
+ class BI::TestModelValue
34
+ include BI::SharedValue
35
+
36
+ def self.update_class
37
+ ::TestModel
38
+ end
39
+
40
+ field type: 'string', db: 'gorm:"type:uuid;primary_key"'
41
+ def id
42
+ @object.id
43
+ end
44
+
45
+ field type: '*int64', db: 'gorm:"type:bigint"'
46
+ def number
47
+ 10 ** 10
48
+ end
49
+
50
+ field type: '*time.Time', db: bp_timestamp # ISO timestamp string, e. g. "2018-11-05T15:40:14.362219+01:00"
51
+ def created_at
52
+ iso_timestamp(@object.created_at)
53
+ end
54
+ end
55
+ ```
56
+
57
+ Compute the values to be transfered by accessing @object and define GO types
58
+ and GORM struct tags for these values.
59
+
60
+ Also include `BI::Updater` in `TestModel` like so:
61
+
62
+ ```
63
+ class TestModel < ApplicationRecord
64
+ include BI::Updater
65
+
66
+ has_one :dependent
67
+
68
+ def trigger_dependent_updates
69
+ dependent.schedule_bi_update_job(true)
70
+ end
71
+ end
72
+ ```
73
+
74
+ ## License
75
+
76
+ This software is licensed under the Apache 2.0 license.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ # vim: set filetype=ruby et sw=2 ts=2:
2
+
3
+ require 'gem_hadar'
4
+
5
+ GemHadar do
6
+ name 'betterplace-bi'
7
+ path_name 'bi'
8
+ path_module 'BI'
9
+ author 'Florian Frank'
10
+ email 'flori@ping.de'
11
+ homepage "https://github.com/betterplace/#{name}"
12
+ summary 'Business Intelligence library'
13
+ description 'This library contains functionality to send/store BI and AB-testing data'
14
+ test_dir 'spec'
15
+ ignore '.*.sw[pon]', 'pkg', 'Gemfile.lock', 'coverage', '.rvmrc',
16
+ '.AppleDouble', '.DS_Store', '.byebug_history', 'errors.lst', 'tags'
17
+
18
+ readme 'README.md'
19
+ title "#{name.camelize} -- BI library"
20
+ licenses << 'Apache-2.0'
21
+
22
+ dependency 'json'
23
+ dependency 'tins'
24
+ dependency 'complex_config'
25
+ dependency 'globalid'
26
+ dependency 'excon'
27
+ dependency 'rails'
28
+ dependency 'sinatra'
29
+ dependency 'betterlog'
30
+ dependency 'mize'
31
+ development_dependency 'rake'
32
+ development_dependency 'simplecov'
33
+ development_dependency 'rspec'
34
+ development_dependency 'byebug'
35
+ end
36
+
37
+ task :default => :spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.7.0
@@ -0,0 +1,39 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # stub: betterplace-bi 0.7.0 ruby lib
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "betterplace-bi".freeze
6
+ s.version = "0.7.0".freeze
7
+
8
+ s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
+ s.require_paths = ["lib".freeze]
10
+ s.authors = ["Florian Frank".freeze]
11
+ s.date = "2024-04-08"
12
+ s.description = "This library contains functionality to send/store BI and AB-testing data".freeze
13
+ s.email = "flori@ping.de".freeze
14
+ s.extra_rdoc_files = ["README.md".freeze, "lib/betterplace-bi.rb".freeze, "lib/bi.rb".freeze, "lib/bi/ab_test_helper.rb".freeze, "lib/bi/api.rb".freeze, "lib/bi/commands.rb".freeze, "lib/bi/commands/base.rb".freeze, "lib/bi/commands/collector.rb".freeze, "lib/bi/commands/connection.rb".freeze, "lib/bi/commands/delete.rb".freeze, "lib/bi/commands/serializer.rb".freeze, "lib/bi/commands/update.rb".freeze, "lib/bi/commands_job.rb".freeze, "lib/bi/event.rb".freeze, "lib/bi/planning_value_parser.rb".freeze, "lib/bi/planning_value_validations.rb".freeze, "lib/bi/railtie.rb".freeze, "lib/bi/request_analyzer.rb".freeze, "lib/bi/session_id.rb".freeze, "lib/bi/shared_value.rb".freeze, "lib/bi/tracking.rb".freeze, "lib/bi/type_generator.rb".freeze, "lib/bi/update_error.rb".freeze, "lib/bi/updater.rb".freeze, "lib/bi/version.rb".freeze]
15
+ s.files = [".gitignore".freeze, ".semaphore/semaphore.yml".freeze, ".tool-versions".freeze, ".utilsrc".freeze, "Gemfile".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "betterplace-bi.gemspec".freeze, "lib/betterplace-bi.rb".freeze, "lib/bi.rb".freeze, "lib/bi/ab_test_helper.rb".freeze, "lib/bi/api.rb".freeze, "lib/bi/commands.rb".freeze, "lib/bi/commands/base.rb".freeze, "lib/bi/commands/collector.rb".freeze, "lib/bi/commands/connection.rb".freeze, "lib/bi/commands/delete.rb".freeze, "lib/bi/commands/serializer.rb".freeze, "lib/bi/commands/update.rb".freeze, "lib/bi/commands_job.rb".freeze, "lib/bi/event.rb".freeze, "lib/bi/planning_value_parser.rb".freeze, "lib/bi/planning_value_validations.rb".freeze, "lib/bi/railtie.rb".freeze, "lib/bi/request_analyzer.rb".freeze, "lib/bi/session_id.rb".freeze, "lib/bi/shared_value.rb".freeze, "lib/bi/tracking.rb".freeze, "lib/bi/type_generator.rb".freeze, "lib/bi/update_error.rb".freeze, "lib/bi/updater.rb".freeze, "lib/bi/version.rb".freeze, "lib/tasks/bime.rake".freeze, "spec/bi/ab_test_helper_spec.rb".freeze, "spec/bi/commands/collector_spec.rb".freeze, "spec/bi/commands/connection_spec.rb".freeze, "spec/bi/commands_job_spec.rb".freeze, "spec/bi/commands_spec.rb".freeze, "spec/bi/event_spec.rb".freeze, "spec/bi/planning_value_parser_spec.rb".freeze, "spec/bi/request_analyzer_spec.rb".freeze, "spec/bi/shared_value_spec.rb".freeze, "spec/bi/tracking_spec.rb".freeze, "spec/bi/type_generator_spec.rb".freeze, "spec/bi/updater_spec.rb".freeze, "spec/bime_dir/.keep".freeze, "spec/config/bi.yml".freeze, "spec/spec_helper.rb".freeze, "spec/support/models.rb".freeze]
16
+ s.homepage = "https://github.com/betterplace/betterplace-bi".freeze
17
+ s.licenses = ["Apache-2.0".freeze]
18
+ s.rdoc_options = ["--title".freeze, "Betterplace-bi -- BI library".freeze, "--main".freeze, "README.md".freeze]
19
+ s.rubygems_version = "3.4.21".freeze
20
+ s.summary = "Business Intelligence library".freeze
21
+ s.test_files = ["spec/bi/ab_test_helper_spec.rb".freeze, "spec/bi/commands/collector_spec.rb".freeze, "spec/bi/commands/connection_spec.rb".freeze, "spec/bi/commands_job_spec.rb".freeze, "spec/bi/commands_spec.rb".freeze, "spec/bi/event_spec.rb".freeze, "spec/bi/planning_value_parser_spec.rb".freeze, "spec/bi/request_analyzer_spec.rb".freeze, "spec/bi/shared_value_spec.rb".freeze, "spec/bi/tracking_spec.rb".freeze, "spec/bi/type_generator_spec.rb".freeze, "spec/bi/updater_spec.rb".freeze, "spec/spec_helper.rb".freeze, "spec/support/models.rb".freeze]
22
+
23
+ s.specification_version = 4
24
+
25
+ s.add_development_dependency(%q<gem_hadar>.freeze, ["~> 1.11.0".freeze])
26
+ s.add_development_dependency(%q<rake>.freeze, [">= 0".freeze])
27
+ s.add_development_dependency(%q<simplecov>.freeze, [">= 0".freeze])
28
+ s.add_development_dependency(%q<rspec>.freeze, [">= 0".freeze])
29
+ s.add_development_dependency(%q<byebug>.freeze, [">= 0".freeze])
30
+ s.add_runtime_dependency(%q<json>.freeze, [">= 0".freeze])
31
+ s.add_runtime_dependency(%q<tins>.freeze, [">= 0".freeze])
32
+ s.add_runtime_dependency(%q<complex_config>.freeze, [">= 0".freeze])
33
+ s.add_runtime_dependency(%q<globalid>.freeze, [">= 0".freeze])
34
+ s.add_runtime_dependency(%q<excon>.freeze, [">= 0".freeze])
35
+ s.add_runtime_dependency(%q<rails>.freeze, [">= 0".freeze])
36
+ s.add_runtime_dependency(%q<sinatra>.freeze, [">= 0".freeze])
37
+ s.add_runtime_dependency(%q<betterlog>.freeze, [">= 0".freeze])
38
+ s.add_runtime_dependency(%q<mize>.freeze, [">= 0".freeze])
39
+ end
@@ -0,0 +1 @@
1
+ require 'bi'
@@ -0,0 +1,81 @@
1
+ module BI
2
+ module ABTestHelper
3
+ OVERRIDE_PARAM_NAME = "ab_test"
4
+
5
+ # Parameter +name+ is the name of the AB-test, the first element of
6
+ # +alternatives+ is the control. +request+ is a ActionDispatch::Request
7
+ # object.
8
+ def ab_test(name, *alternatives, request: self.request, &block)
9
+ alternatives.flatten!
10
+ alternatives.empty? and return
11
+ session_id, mode = nil, nil
12
+ chosen =
13
+ case
14
+ when override = ab_test_override?(name, *alternatives, request: request)
15
+ mode = :override
16
+ override
17
+ when ab_test_ignore_user?(request: request)
18
+ mode = :ignore
19
+ alternatives.first
20
+ when session = request.ask_and_send(:session)
21
+ session.loaded? or session.send(:load!)
22
+ session_id = BI::SessionID.session_id(session)
23
+ n = if session_id.empty?
24
+ mode = :random
25
+ rand(1 << 128)
26
+ else
27
+ mode = :session
28
+ Digest::MD5.hexdigest([ name, session_id ] * ?:).to_i(16)
29
+ end
30
+ alternatives[n % alternatives.size]
31
+ else
32
+ mode = :default
33
+ n = rand(1 << 128)
34
+ alternatives[n % alternatives.size]
35
+ end
36
+ Log.info(
37
+ "session.id=#{session_id} => #{chosen} was chosen for #{name} (#{mode})",
38
+ meta: { module: 'bi' }
39
+ )
40
+ block&.(chosen) || chosen
41
+ end
42
+
43
+ def ab_test_ignore_user?(request: self.request)
44
+ request and BI::RequestAnalyzer.new(request).ignore?
45
+ end
46
+
47
+ # It's possible to pick the control with the URL parameter
48
+ # 'ab_test=control' or ab_test=0, the first alternative by
49
+ # ab_test=alternative or ab_test=1, further alternatives by 2, 3,… It's
50
+ # also possible to pick one by using the version's name like ab_test=foo.
51
+ # If there is more than one AB-Test running, this parameter can be
52
+ # ab_test[name]=… to only switch the AB-Test
53
+ # named +name+ to the control or alternatives. This way it's possible to
54
+ # view all the possible permutations of controls and alternatives if you're
55
+ # crazy enough.
56
+ def ab_test_override?(name, *alternatives, request: self.request)
57
+ alternatives.flatten!
58
+ overriden = request.params[OVERRIDE_PARAM_NAME] rescue nil
59
+ case
60
+ when overriden.respond_to?(:to_str)
61
+ ab_test_override_alternative(overriden.to_str, *alternatives)
62
+ when overriden.respond_to?(:to_hash)
63
+ ab_test_override_alternative(overriden.to_hash[name], *alternatives)
64
+ end
65
+ end
66
+
67
+ def ab_test_override_alternative(override, *alternatives)
68
+ alternatives.flatten!
69
+ case override
70
+ when 'control'
71
+ alternatives.first
72
+ when 'alternative'
73
+ alternatives.second
74
+ when /\A\d+\z/
75
+ alternatives[$&.to_i] || alternatives.first
76
+ when *alternatives
77
+ override
78
+ end
79
+ end
80
+ end
81
+ end
data/lib/bi/api.rb ADDED
@@ -0,0 +1,115 @@
1
+ require 'sinatra/base'
2
+
3
+ module BI
4
+ module API
5
+
6
+ module Helper
7
+ include BI::ABTestHelper
8
+
9
+ def self.included(modul)
10
+ modul.extend self
11
+ end
12
+
13
+ def hostname
14
+ @hostname ||= Socket.gethostname rescue 'n/a'
15
+ end
16
+
17
+ def status_code(symbol)
18
+ Rack::Utils::SYMBOL_TO_STATUS_CODE[symbol]
19
+ end
20
+
21
+ def rails_request
22
+ ActionDispatch::Request.new(env)
23
+ end
24
+
25
+ def rails_session
26
+ rails_request.session
27
+ end
28
+
29
+ def render_result(result, code: :ok, headers: {})
30
+ headers = headers.merge(
31
+ 'Content-Type' => 'application/json',
32
+ )
33
+ code = status_code(code)
34
+ [ code, headers, result ]
35
+ end
36
+
37
+ def render_not_found
38
+ result = JSON(
39
+ hostname: hostname,
40
+ message: 'not found',
41
+ )
42
+ render_result(result, code: :not_found)
43
+ end
44
+
45
+ def render_exception(exception, code: :internal_server_error)
46
+ result = JSON(
47
+ hostname: hostname,
48
+ error: exception.class.name,
49
+ message: exception.message,
50
+ )
51
+ render_result(result, code: code)
52
+ end
53
+
54
+ def ab_test_result(channel, request)
55
+ config = cc.ab_tests[channel.to_s] or return
56
+ analyzer = BI::RequestAnalyzer.new(request)
57
+ current = ab_test(config.name, config.alternatives, request: rails_request)
58
+ version = "#{config.name}/#{current}"
59
+ {
60
+ channel: channel,
61
+ version: version,
62
+ session: BI::SessionID.session_id(request.session),
63
+ device_type: analyzer.device_type,
64
+ user_agent: analyzer.user_agent,
65
+ }
66
+ end
67
+ end
68
+
69
+ class App < Sinatra::Base
70
+ include BI::API::Helper
71
+
72
+ before do
73
+ rr = rails_request
74
+ Log.info "Received #{rr.method} to #{rr.path}",
75
+ type: 'bi_request',
76
+ meta: {
77
+ module: 'bi',
78
+ url: rr.url,
79
+ method: rr.method,
80
+ params: rr.params,
81
+ referer: rr.referer,
82
+ }
83
+ end
84
+
85
+ get '/' do
86
+ render_result JSON(
87
+ endpoints: {
88
+ '/bi' => { methods: [ 'GET' ] },
89
+ '/bi/ab_test/:channel' => { methods: [ 'GET', 'POST' ] },
90
+ }
91
+ )
92
+ end
93
+
94
+ get '/ab_test/:channel' do
95
+ channel = params.fetch('channel')
96
+ result = ab_test_result(channel, rails_request) or return
97
+ render_result JSON(result)
98
+ end
99
+
100
+ post '/ab_test/:channel' do
101
+ channel = params.fetch('channel')
102
+ result = ab_test_result(channel, rails_request) or render_not_found
103
+ event_data = JSON(
104
+ request.body.tap(&:rewind).read
105
+ ).symbolize_keys_recursive
106
+ event_data.update result
107
+ BI::Event.new.write(**event_data)
108
+ render_result JSON(result)
109
+ rescue => e
110
+ Log.error e, meta: { module: 'bi' }
111
+ render_exception(e, code: :unprocessable_entity)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,11 @@
1
+ module BI
2
+ module Commands
3
+ class Base
4
+ attr_reader :data
5
+
6
+ def initialize(data)
7
+ @data = data
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ module BI
2
+ module Commands
3
+ class Collector
4
+ def initialize
5
+ @first_object = nil
6
+ @commands = []
7
+ end
8
+
9
+ def add(object, value_class)
10
+ @first_object ||= object
11
+ if object&.destroyed?
12
+ if url = value_class.url(method: 'DELETE', id: object.id)
13
+ @commands << BI::Commands::Delete.new(url: url)
14
+ end
15
+ else
16
+ value = value_class.new(object)
17
+ @commands << BI::Commands::Update.new(value: value)
18
+ end
19
+ self
20
+ end
21
+
22
+ def schedule_job?(object)
23
+ @first_object == object
24
+ end
25
+
26
+ def schedule_job
27
+ BI::CommandsJob.new(*@commands).enqueue
28
+ self
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ module BI
2
+ module Commands
3
+ module Connection
4
+ module_function
5
+
6
+ def create_connection
7
+ Log.info("Connecting BI Server at %s now" % cc.bi.server)
8
+ Excon.new(
9
+ cc.bi.server,
10
+ user: cc.bi.auth.username,
11
+ password: cc.bi.auth.password,
12
+ persistent: true,
13
+ )
14
+ end
15
+
16
+ def connect_to_server
17
+ connection = create_connection
18
+ yield connection
19
+ self
20
+ ensure
21
+ connection&.reset
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ require 'bi/commands/base'
2
+
3
+ module BI
4
+ module Commands
5
+ class Delete < BI::Commands::Base
6
+
7
+ def url
8
+ data[:url]
9
+ end
10
+
11
+ def perform_via(connection)
12
+ Log.info("Sending BI Delete to #{url}")
13
+ response = connection.delete(
14
+ path: url,
15
+ headers: {
16
+ 'Content-Type' => 'application/json',
17
+ 'Accept' => 'application/json',
18
+ }
19
+ )
20
+ case response.status
21
+ when 0...300
22
+ Log.info "Successfully sent delete to #{url}."
23
+ when 404
24
+ Log.warn "Duplicate deletion sent to #{url}: status=#{response.status}\n#{response.body}"
25
+ # Already deleted
26
+ else
27
+ Log.warn "Failed to sent delete to #{url}: status=#{response.status}\n#{response.body}"
28
+ raise BI::UpdateError, "status=#{response.status}: #{response.body}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ module BI
2
+ module Commands
3
+ class Serializer < ActiveJob::Serializers::ObjectSerializer
4
+ # Checks if an argument should be serialized by this serializer.
5
+ def serialize?(argument)
6
+ argument.is_a? ::BI::Commands::Base
7
+ end
8
+
9
+ GLOBALID_KEY = "_bi_globalid"
10
+
11
+ # Converts an object to a simpler representative using supported object
12
+ # types. The recommended representative is a Hash with a specific key. Keys
13
+ # can be of basic types only. You should call `super` to add the custom
14
+ # serializer type to the hash.
15
+ def serialize(command)
16
+ hash = { class: command.class.name }
17
+ hash |= command.data.to_h.transform_values { |v|
18
+ if v.is_a?(GlobalID::Identification)
19
+ v = { GLOBALID_KEY => v.to_global_id.to_s }
20
+ end
21
+ v
22
+ }
23
+ hash.stringify_keys!
24
+ super(hash)
25
+ end
26
+
27
+ # Converts serialized value into a proper object.
28
+ def deserialize(hash)
29
+ hash.delete("class").constantize.new(
30
+ hash.transform_values { |v|
31
+ if h = v.ask_and_send(:to_hash) and h.include?(GLOBALID_KEY)
32
+ v = GlobalID::Locator.locate h[GLOBALID_KEY]
33
+ end
34
+ v
35
+ }.symbolize_keys
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ require 'bi/commands/base'
2
+
3
+ module BI
4
+ module Commands
5
+ class Update < BI::Commands::Base
6
+ def value
7
+ data[:value]
8
+ end
9
+
10
+ def perform_via(connection)
11
+ Log.info("Sending BI Update value for %s" % value)
12
+ response = connection.post(
13
+ path: value.url,
14
+ headers: {
15
+ 'Content-Type' => 'application/json',
16
+ 'Accept' => 'application/json',
17
+ },
18
+ body: value.to_json,
19
+ )
20
+ if response.status < 300
21
+ Log.info "Successfully sent update for #{value}."
22
+ else
23
+ Log.warn "Failed to sent update for #{value}: status=#{response.status}\n#{response.body}"
24
+ raise BI::UpdateError, "status=#{response.status}: #{response.body}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ module BI
2
+ module Commands
3
+ end
4
+ end
5
+
6
+ require 'bi/commands/connection'
7
+ require 'bi/commands/update'
8
+ require 'bi/commands/delete'
9
+ require 'bi/commands/collector'
10
+ require 'bi/commands/serializer' if defined?(ActiveJob::Serializers)
@@ -0,0 +1,24 @@
1
+ require 'active_job/arguments'
2
+
3
+ module BI
4
+ class CommandsJob < ::ActiveJob::Base
5
+ include BI::Commands::Connection
6
+
7
+ queue_as :bime
8
+
9
+ retry_on Excon::Error::Socket, # handle temporary dns resolution problems
10
+ Excon::Error::Timeout # handle timeouts
11
+
12
+ retry_on(ActiveJob::DeserializationError, wait: :polynomially_longer, attempts: 4) {}
13
+
14
+ def perform(*commands)
15
+ commands.empty? and return
16
+ connect_to_server do |connection|
17
+ commands.each do |command|
18
+ command.perform_via(connection)
19
+ end
20
+ end
21
+ self
22
+ end
23
+ end
24
+ end