betterplace-bi 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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