shatter-rb 0.0.1

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +20 -0
  4. data/.ruby-version +1 -0
  5. data/.tool-versions +1 -0
  6. data/ApacheJMeter.jar +0 -0
  7. data/CHANGELOG.md +5 -0
  8. data/CODE_OF_CONDUCT.md +84 -0
  9. data/Gemfile +8 -0
  10. data/Gemfile.lock +94 -0
  11. data/LICENSE.txt +21 -0
  12. data/Procfile +5 -0
  13. data/README.md +39 -0
  14. data/Rakefile +12 -0
  15. data/example_app/Gemfile +5 -0
  16. data/example_app/Gemfile.lock +50 -0
  17. data/example_app/app/functions/hello_world_function.rb +19 -0
  18. data/example_app/app/service_definition.rb +9 -0
  19. data/example_app/application.rb +6 -0
  20. data/example_app/bin/console +11 -0
  21. data/example_app/bin/server +4 -0
  22. data/example_app/bin/service +16 -0
  23. data/example_app/config/environment.rb +9 -0
  24. data/example_app/config.ru +9 -0
  25. data/example_app/docker-compose.yml +32 -0
  26. data/exe/console +12 -0
  27. data/exe/shatter +64 -0
  28. data/lib/shatter/config.rb +12 -0
  29. data/lib/shatter/service/base.rb +93 -0
  30. data/lib/shatter/service/discovery.rb +49 -0
  31. data/lib/shatter/service/function.rb +56 -0
  32. data/lib/shatter/service/function_params.rb +21 -0
  33. data/lib/shatter/service/response_pool.rb +10 -0
  34. data/lib/shatter/service/service_definition.rb +21 -0
  35. data/lib/shatter/util.rb +19 -0
  36. data/lib/shatter/version.rb +5 -0
  37. data/lib/shatter/web/application.rb +50 -0
  38. data/lib/shatter/web/server.rb +45 -0
  39. data/lib/shatter.rb +21 -0
  40. data/shatter.gemspec +54 -0
  41. data/sig/shatter.rbs +4 -0
  42. data/templates/Gemfile.template +5 -0
  43. data/templates/application.erb +6 -0
  44. data/templates/config.ru +9 -0
  45. data/templates/docker-compose.yml +32 -0
  46. data/templates/docker-compose.yml.example +32 -0
  47. data/templates/environment.rb.erb +9 -0
  48. data/templates/hello_world_function.rb.erb +17 -0
  49. data/templates/service_definition.rb.erb +9 -0
  50. metadata +278 -0
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+ require 'benchmark'
4
+ require "drb/drb"
5
+ require "zk"
6
+ require "concurrent-ruby"
7
+
8
+ # Server Side of the drb
9
+ module Shatter
10
+ module Service
11
+ class Base
12
+ include Concurrent::Async
13
+
14
+ class ZooKeeperConnection
15
+ include Singleton
16
+ attr_reader :client
17
+ def initialize
18
+ @client = ZK.new(Shatter::Config.zookeeper_host)
19
+ end
20
+ end
21
+
22
+ class << self
23
+ attr_reader :service_definition
24
+ attr_writer :service_definition
25
+ end
26
+
27
+ def self.response_for(uuid)
28
+ ResponsePool.instance.pool[uuid]
29
+ end
30
+
31
+ def self.respond_to_missing?(method)
32
+ @service_definition.instance_methods.include?(method.to_sym)
33
+ end
34
+
35
+ def self.set_static_result_for(uuid, result)
36
+ puts "Setting static content"
37
+ populate_pool_with_result(Time.now, {uuid:, result:}, nil)
38
+ end
39
+
40
+ def self.method_missing(method, *args, &)
41
+ Shatter.logger.info "Mapping #{method}"
42
+ uuid = args[0].is_a?(Hash) ? args[0][:uuid] : args[0].uuid
43
+ return {error: 'missing uuid'} if uuid.nil?
44
+ future = @service_definition.new.async.send(method, *args, &)
45
+ future.add_observer(self, :populate_pool_with_result)
46
+ end
47
+
48
+ def self.populate_pool_with_result(time, value, err)
49
+ if err
50
+ Shatter.logger.info err
51
+ end
52
+ ResponsePool.instance.pool[value[:uuid]] = value
53
+ zk = ZooKeeperConnection.instance.client
54
+ key = nil
55
+ key = Util.zookeeper_response_key(value[:uuid])
56
+ my_ip = ENV["HOST_NAME"] || "localhost"
57
+ host = "#{my_ip}:#{Shatter::Config.service_port}"
58
+ Shatter.logger.info "Recording location of #{value[:uuid]} at #{host} #{value}"
59
+ zk.create(key, host)
60
+ my_ip
61
+ rescue Exception => e
62
+ Shatter.logger.error e
63
+ raise e
64
+ end
65
+
66
+ def self.close
67
+ logger = Shatter.logger
68
+ logger.info "Closing down DRb service"
69
+ port = Shatter::Config.service_port
70
+ uri = "localhost:#{port}"
71
+ logger.info "Removing my existnce at #{port} to zookeeper"
72
+ Shatter::Service::Discovery.deregister_service(uri)
73
+ logger.info "Closed DRb service"
74
+ end
75
+
76
+ def self.init
77
+ logger.info "Initing DRb service"
78
+ port = Shatter::Config.service_port
79
+ uri = "localhost:#{port}"
80
+ logger.info "Logging my existnce at #{uri} to zookeeper"
81
+ Shatter::Service::Discovery.register_service(uri)
82
+ logger.info "Starting DRb service"
83
+ DRb.start_service("druby://#{uri}", self)
84
+ logger.info "DRb service started"
85
+ DRb.thread.join
86
+ end
87
+
88
+ def self.logger
89
+ Shatter.logger
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,49 @@
1
+ module Shatter
2
+ module Service
3
+ class Discovery
4
+ class << self
5
+
6
+ def deregister_service(service_url)
7
+ zk = ZK.new (Shatter::Config.zookeeper_host)
8
+ if zk.exists?("/shater_service_instances/#{service_url}")
9
+ zk.delete("/shater_service_instances/#{service_url}")
10
+ end
11
+ zk.close
12
+ end
13
+
14
+ def register_service(service_url)
15
+ Shatter.logger.info "Registering #{service_url} to zookeeper"
16
+ zk = ZK.new (Shatter::Config.zookeeper_host)
17
+ unless zk.exists?("/shater_service_instances/#{service_url}")
18
+ created = zk.create("/shater_service_instances/#{service_url}")
19
+ Shatter.logger.info "Registered #{created}"
20
+ end
21
+ puts zk.children("/shater_service_instances")
22
+ zk.close
23
+ end
24
+
25
+ def service_instance_url
26
+ service_instance_urls.sample
27
+ end
28
+
29
+ def service_instance_urls
30
+ zk = ZK.new (Shatter::Config.zookeeper_host)
31
+ urls = zk.children("/shater_service_instances")
32
+ zk.close
33
+ urls
34
+ end
35
+
36
+ def service_instance_url_for_uuid(uuid)
37
+ druby_instance_url = nil
38
+ key = Util.zookeeper_response_key(uuid)
39
+ zk = ZK.new(Shatter::Config.zookeeper_host)
40
+ druby_instance_url = zk.get(key)[0] if zk.exists?(key)
41
+ zk.close
42
+ Shatter.logger.debug "Service instance url for #{uuid} - #{druby_instance_url}"
43
+
44
+ druby_instance_url
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,56 @@
1
+ module Shatter
2
+ module Service
3
+ class Function
4
+
5
+ def self.define_param(name, type:, nullable: true)
6
+ @@param_meta ||= {}
7
+ @@param_meta[name] = {name:, type:, nullable:}
8
+ end
9
+
10
+ def self.meta
11
+ @@param_meta
12
+ end
13
+
14
+ define_param :uuid, nullable: false, type: 'string'
15
+
16
+ def initialize(function_params)
17
+ @params = function_params
18
+ end
19
+
20
+ def params
21
+ Hash[@@param_meta.keys.map { |k| [k]}].merge(@params)
22
+ end
23
+
24
+ def call
25
+ raise 'Invalid Args' unless valid_params?
26
+ valid_keys = @@param_meta.keys
27
+ {result: nil, error:nil}.merge(self.invoke.merge(uuid: params[:uuid]))
28
+ rescue Exception => e
29
+ {result: nil, error: e.message, uuid: params[:uuid]}
30
+ end
31
+
32
+ def valid_params?
33
+ self.class.meta.keys.each do |arg|
34
+ meta = self.class.meta[arg]
35
+ val = @params[arg]
36
+ puts meta
37
+ raise "#{arg} cannot be nil" if !meta[:nullable] && val.nil?
38
+ raise "expected #{arg} to be a string" if meta[:type] == 'string' && !val.is_a?(String)
39
+
40
+ if meta[:type] == 'integer'
41
+ if !meta[:nullable] && val.nil?
42
+ raise 'value cannot be nil'
43
+ end
44
+ if !val.nil? && !val.is_a?(Integer)
45
+ raise "expected #{arg} to be an integer"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.invoke
52
+ raise 'cant invoke for base function'
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,21 @@
1
+ module Shatter
2
+ module Service
3
+ class FunctionParams < Data
4
+
5
+ def self.generate(*args, &block)
6
+ args << :uuid
7
+
8
+ Data.define(*args) do
9
+ def to_typescript
10
+ typescript_name = self.class.to_s.split("::").last(2).join
11
+ out = <<-HEREDOC.gsub(/^\s+/, "")
12
+ type #{typescript_name} {
13
+ };
14
+ export default #{typescript_name}
15
+ HEREDOC
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ require 'singleton'
2
+ module Shatter
3
+ class ResponsePool
4
+ include Singleton
5
+ attr_reader :pool
6
+ def initialize
7
+ @pool = {}
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ require "concurrent-ruby"
2
+ require 'set'
3
+
4
+ module Shatter
5
+ module Service
6
+ class ServiceDefinition
7
+ include Concurrent::Async
8
+ class << self
9
+ attr_reader :function_collection
10
+ end
11
+ def self.register_function(identifier, function)
12
+ Shatter.logger.info "Registering function - #{identifier} for #{self}"
13
+ @function_collection ||= {}
14
+ @function_collection[identifier.to_s] = function
15
+ define_method identifier do |params|
16
+ function.new(params).call
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ require 'logger'
3
+ require 'singleton'
4
+
5
+ module Shatter
6
+ module Util
7
+ class Logger < ::Logger
8
+ include Singleton
9
+ def initialize
10
+ super(STDOUT, datetime_format: '%Y-%m-%d %H:%M:%S')
11
+ end
12
+ end
13
+
14
+ def self.zookeeper_response_key(uuid)
15
+ raise 'Cant produce key without uuid' if uuid.nil?
16
+ "/shatter::response_data_locations/#{uuid}"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shatter
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "drb/drb"
4
+ require "concurrent-ruby"
5
+
6
+ require_relative "./application"
7
+
8
+ module Shatter
9
+ module Web
10
+ class Application
11
+ include Concurrent::Async
12
+
13
+ def response_for(uuid)
14
+ druby_instance_url = Shatter::Service::Discovery.service_instance_url_for_uuid(uuid)
15
+ return unless druby_instance_url
16
+ DRbObject.new_with_uri("druby://#{druby_instance_url}").response_for(uuid)
17
+ end
18
+
19
+ def route(uuid, path, params)
20
+ operation = path.scan(/\/(.+)$/).first&.first
21
+ raise 'failed to route' if operation.nil?
22
+ Shatter.logger.info Shatter::Service::Base.service_definition.function_collection
23
+ Shatter.logger.info operation
24
+ function = Shatter::Service::Base.service_definition.function_collection[operation]
25
+ if function.nil?
26
+ DRbObject.new_with_uri("druby://#{Shatter::Service::Discovery.service_instance_url}")
27
+ .set_static_result_for(uuid, {result: nil, error: :unknown_operation})
28
+ else
29
+ begin
30
+ func_params = params.merge(uuid:)
31
+ Shatter.logger.info "routing #{path}/#{func_params}"
32
+ DRbObject.new_with_uri("druby://#{Shatter::Service::Discovery.service_instance_url}")
33
+ .send(
34
+ operation.to_sym,
35
+ func_params
36
+ )
37
+ rescue ArgumentError => e
38
+ DRbObject.new_with_uri("druby://#{Shatter::Service::Discovery.service_instance_url}")
39
+ .set_static_result_for(uuid, {result: nil, error: e.message })
40
+ end
41
+ end
42
+ rescue Exception => e
43
+ Shatter.logger.error "caught error: #{e.message}"
44
+ Shatter.logger.error "#{e.backtrace.join("\n")}"
45
+ { data:, error: e.message, uuid: }
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "json"
5
+
6
+ module Shatter
7
+ module Web
8
+ class Server
9
+
10
+ class << self
11
+ attr_reader :application
12
+ attr_writer :application
13
+ end
14
+
15
+ def self.call(env)
16
+ request = Rack::Request.new(env)
17
+ path = env["PATH_INFO"]
18
+ if env["PATH_INFO"] == "/callbacks"
19
+ uuid = env['QUERY_STRING'].split("=")[1]
20
+ response_for(uuid)
21
+ elsif env["PATH_INFO"] == "/base_time"
22
+ [200, {}, [""]]
23
+ else
24
+ params = JSON.parse(request.body.read, {symbolize_names: true})
25
+ uuid = SecureRandom.uuid
26
+ future = application.new.async.route(uuid, path, params)
27
+ future.add_observer(:server_call_result, self)
28
+ [200, { "delay" => "20", "location" => "/callbacks?uuid=#{uuid}" }, []]
29
+ end
30
+ end
31
+
32
+ def self.server_call_result(time, value, error)
33
+ return if error.nil?
34
+ Shatter.logger.error "#{ error }"
35
+ end
36
+
37
+ def self.response_for(uuid)
38
+ response = application.new.response_for(uuid)
39
+ return [200, {}, [response.to_json]] unless response.nil?
40
+
41
+ [200, { "delay" => "100", "location" => "/callbacks?uuid=#{uuid}" }, []]
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/shatter.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shatter/version"
4
+ require_relative "shatter/web/application"
5
+ require_relative "shatter/config"
6
+ require_relative "shatter/web/server"
7
+ require_relative "shatter/service/base"
8
+ require_relative "shatter/service/discovery"
9
+ require_relative "shatter/service/service_definition"
10
+ require_relative "shatter/service/response_pool"
11
+ require_relative "shatter/util"
12
+
13
+ $stdout.sync = true
14
+
15
+ module Shatter
16
+ class Error < StandardError; end
17
+ def self.logger
18
+ Util::Logger.instance
19
+ end
20
+ # Your code goes here...
21
+ end
data/shatter.gemspec ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/shatter/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "shatter-rb"
7
+ spec.version = Shatter::VERSION
8
+ spec.authors = ["Eric Roos"]
9
+ spec.email = ["ericroos@hey.com"]
10
+
11
+ spec.summary = "Write a short summary, because RubyGems requires one."
12
+ spec.description = "Write a longer description or delete this line."
13
+ spec.homepage = "https://rubygems.org/gems/shatter"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1.0"
16
+
17
+ #spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/EricRoos/shatter"
21
+ spec.metadata["changelog_uri"] = "https://github.com/EricRoos/shatter/tree/master/CHANGELOG"
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:javascript|bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ # spec.add_dependency "example-gem", "~> 1.0"
36
+ spec.add_dependency "rack", "~> 3.0"
37
+ spec.add_dependency "rackup", "~> 0.2.3"
38
+ spec.add_dependency "concurrent-ruby", "~> 1.1"
39
+ spec.add_dependency "pg", "~> 1.4"
40
+ spec.add_dependency "zk", "~> 1.10"
41
+ spec.add_dependency "puma", "~> 6.0"
42
+ spec.add_dependency "thor", "~> 1.2.1"
43
+ spec.add_dependency "erb", "~> 2.2.0"
44
+ # spec.add_dependency "zookeeper", "~>1.5.4", github: 'EricRoos/zookeeper'
45
+
46
+ spec.add_development_dependency "rake", "~> 13.0"
47
+ spec.add_development_dependency "rspec", "~> 3.0"
48
+ spec.add_development_dependency "rubocop", "~> 1.21"
49
+ spec.add_development_dependency "rubocop-rake"
50
+ spec.add_development_dependency "rubocop-rspec"
51
+ # For more information and examples about making a new gem, check out our
52
+ # guide at: https://bundler.io/guides/creating_gem.html
53
+ #spec.metadata["rubygems_mfa_required"] = "true"
54
+ end
data/sig/shatter.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Shatter
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+ gem 'shatter', path: '..'
3
+ gem "zookeeper", "~>1.5.4", github: 'EricRoos/zookeeper'
4
+
5
+ gem "foreman", "~> 0.87.2"
@@ -0,0 +1,6 @@
1
+ module <%= app_name %>
2
+ class Application < Shatter::Web::Application
3
+ end
4
+ end
5
+
6
+ require_relative './app/service_definition'
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "shatter"
5
+
6
+ require_relative "./config/environment"
7
+
8
+ use Rack::CommonLogger
9
+ run Shatter::Web::Server
@@ -0,0 +1,32 @@
1
+ version: '3.1'
2
+
3
+ services:
4
+ zoo1:
5
+ image: zookeeper
6
+ restart: always
7
+ hostname: zoo1
8
+ ports:
9
+ - 2181:2181
10
+ environment:
11
+ ZOO_MY_ID: 1
12
+ ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
13
+
14
+ zoo2:
15
+ image: zookeeper
16
+ restart: always
17
+ hostname: zoo2
18
+ ports:
19
+ - 2182:2181
20
+ environment:
21
+ ZOO_MY_ID: 2
22
+ ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
23
+
24
+ zoo3:
25
+ image: zookeeper
26
+ restart: always
27
+ hostname: zoo3
28
+ ports:
29
+ - 2183:2181
30
+ environment:
31
+ ZOO_MY_ID: 3
32
+ ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
@@ -0,0 +1,32 @@
1
+ version: '3.1'
2
+
3
+ services:
4
+ zoo1:
5
+ image: zookeeper
6
+ restart: always
7
+ hostname: zoo1
8
+ ports:
9
+ - 2181:2181
10
+ environment:
11
+ ZOO_MY_ID: 1
12
+ ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
13
+
14
+ zoo2:
15
+ image: zookeeper
16
+ restart: always
17
+ hostname: zoo2
18
+ ports:
19
+ - 2182:2181
20
+ environment:
21
+ ZOO_MY_ID: 2
22
+ ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
23
+
24
+ zoo3:
25
+ image: zookeeper
26
+ restart: always
27
+ hostname: zoo3
28
+ ports:
29
+ - 2183:2181
30
+ environment:
31
+ ZOO_MY_ID: 3
32
+ ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
@@ -0,0 +1,9 @@
1
+ Shatter::Config.zookeeper_host = "localhost:2181"
2
+ Shatter::Config.initial_delay = 100
3
+ Shatter::Config.missing_result_delay = 100
4
+ Shatter::Config.service_port = ENV.fetch('SHATTER_SERVICE_PORT') { 8787 }
5
+
6
+ require_relative '../application'
7
+
8
+ Shatter::Service::Base.service_definition = <%= app_name%>::ServiceDefinition
9
+ Shatter::Web::Server.application = <%= app_name%>::Application
@@ -0,0 +1,17 @@
1
+ require "pg"
2
+ require "shatter/service/function"
3
+ require "shatter/service/function_params"
4
+
5
+ module <%= app_name %>
6
+ module Functions
7
+ class HelloWorldFunction < Shatter::Service::Function
8
+ define_param :name, nullable: false, type: 'string'
9
+ define_param :number, nullable: false, type: 'integer'
10
+
11
+ def invoke
12
+ params.to_h => name:, uuid:
13
+ { result: "Hello #{name}", uuid:, error: nil, uuid: }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './functions/hello_world_function'
4
+
5
+ module <%= app_name %>
6
+ class ServiceDefinition < Shatter::Service::ServiceDefinition
7
+ register_function :hello_world, <%= app_name %>::Functions::HelloWorldFunction
8
+ end
9
+ end