shatter-rb 0.0.1

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