archfiend 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +323 -0
  6. data/.travis.yml +11 -0
  7. data/CHANGES.md +12 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +7 -0
  10. data/Gemfile.lock +122 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +114 -0
  13. data/ROADMAP.md +11 -0
  14. data/Rakefile +6 -0
  15. data/archfiend.gemspec +35 -0
  16. data/bin/console +14 -0
  17. data/bin/setup +8 -0
  18. data/exe/archfiend +4 -0
  19. data/lib/archfiend.rb +18 -0
  20. data/lib/archfiend/application.rb +132 -0
  21. data/lib/archfiend/cli.rb +9 -0
  22. data/lib/archfiend/generators/daemon.rb +144 -0
  23. data/lib/archfiend/generators/daemon/templates/.rspec +4 -0
  24. data/lib/archfiend/generators/daemon/templates/.rubocop.yml +323 -0
  25. data/lib/archfiend/generators/daemon/templates/Gemfile.tt +32 -0
  26. data/lib/archfiend/generators/daemon/templates/README.md.tt +23 -0
  27. data/lib/archfiend/generators/daemon/templates/Rakefile.tt +13 -0
  28. data/lib/archfiend/generators/daemon/templates/app/clockwork/clockwork.rb.tt +14 -0
  29. data/lib/archfiend/generators/daemon/templates/app/models/application_record.rb +3 -0
  30. data/lib/archfiend/generators/daemon/templates/app/subprocess_loops/foo_subprocess_loop.rb +13 -0
  31. data/lib/archfiend/generators/daemon/templates/app/thread_loops/bar_thread_loop.rb +8 -0
  32. data/lib/archfiend/generators/daemon/templates/bin/console +3 -0
  33. data/lib/archfiend/generators/daemon/templates/bin/start.tt +6 -0
  34. data/lib/archfiend/generators/daemon/templates/config/application.rb.tt +27 -0
  35. data/lib/archfiend/generators/daemon/templates/config/boot.rb.tt +3 -0
  36. data/lib/archfiend/generators/daemon/templates/config/daemon.rb.tt +18 -0
  37. data/lib/archfiend/generators/daemon/templates/config/database.yml.example.tt +26 -0
  38. data/lib/archfiend/generators/daemon/templates/config/environment.rb.tt +4 -0
  39. data/lib/archfiend/generators/daemon/templates/config/settings.yml.tt +8 -0
  40. data/lib/archfiend/generators/daemon/templates/config/settings/development.yml.tt +2 -0
  41. data/lib/archfiend/generators/daemon/templates/config/settings/production.yml.tt +2 -0
  42. data/lib/archfiend/generators/daemon/templates/config/settings/staging.yml.tt +5 -0
  43. data/lib/archfiend/generators/daemon/templates/config/settings/test.yml.tt +0 -0
  44. data/lib/archfiend/generators/daemon/templates/db/migrate/.gitkeep +0 -0
  45. data/lib/archfiend/generators/daemon/templates/lib/tasks/.gitkeep +0 -0
  46. data/lib/archfiend/generators/daemon/templates/log/.gitkeep +0 -0
  47. data/lib/archfiend/generators/daemon/templates/spec/factories/.gitkeep +0 -0
  48. data/lib/archfiend/generators/daemon/templates/spec/models/.gitkeep +0 -0
  49. data/lib/archfiend/generators/daemon/templates/spec/spec_helper.rb +43 -0
  50. data/lib/archfiend/generators/daemon/templates/spec/subprocess_loops/foo_subprocess_loop_spec.rb +5 -0
  51. data/lib/archfiend/generators/daemon/templates/spec/support/factory_bot.rb +9 -0
  52. data/lib/archfiend/generators/daemon/templates/spec/support/timecop.rb +9 -0
  53. data/lib/archfiend/generators/daemon/templates/spec/thread_loops/bar_thread_loop_spec.rb +5 -0
  54. data/lib/archfiend/generators/daemon/templates/tmp/.gitkeep +0 -0
  55. data/lib/archfiend/generators/extensions.rb +119 -0
  56. data/lib/archfiend/generators/options.rb +51 -0
  57. data/lib/archfiend/generators/utils.rb +37 -0
  58. data/lib/archfiend/logging.rb +38 -0
  59. data/lib/archfiend/logging/base_formatter.rb +35 -0
  60. data/lib/archfiend/logging/default_formatter.rb +12 -0
  61. data/lib/archfiend/logging/json_formatter.rb +21 -0
  62. data/lib/archfiend/logging/multi_logger.rb +31 -0
  63. data/lib/archfiend/shared_loop/runnable.rb +26 -0
  64. data/lib/archfiend/subprocess_loop.rb +46 -0
  65. data/lib/archfiend/thread_loop.rb +35 -0
  66. data/lib/archfiend/version.rb +3 -0
  67. data/scripts/travis/install +13 -0
  68. data/scripts/travis/script +43 -0
  69. metadata +280 -0
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5
+
6
+ gem 'activesupport', require: 'active_support/time'
7
+ gem 'activerecord', require: 'active_record'
8
+ gem 'activerecord-migrations'
9
+ gem 'archfiend', github: 'toptal/archfiend'
10
+ gem 'clockwork', require: false
11
+ gem 'config'
12
+ gem 'daemons'
13
+ gem 'pg', '~> 0.21'
14
+ gem 'rbtrace', require: false
15
+
16
+ group :development, :test do
17
+ gem 'factory_bot', require: false
18
+ gem 'pry'
19
+ gem 'pry-stack_explorer'
20
+ gem 'pry-byebug'
21
+ gem 'pry-doc', require: false
22
+ gem 'rspec', require: false
23
+ gem 'rubocop', require: false
24
+ gem 'rubocop-rspec', require: false
25
+ gem 'shoulda-matchers', '~> 3.1'
26
+ gem 'timecop', require: false
27
+ gem 'webmock', require: false
28
+ end
29
+
30
+ group :test do
31
+ gem 'database_cleaner'
32
+ end
@@ -0,0 +1,23 @@
1
+ # <%= camelized_daemon_name %>
2
+
3
+ ## Running
4
+
5
+ ### In the foreground
6
+ Start in the foreground (ex. development)
7
+ ```bash
8
+ bin/start
9
+ ```
10
+
11
+ ### Daemonized
12
+ Start daemonized
13
+ ```bash
14
+ bin/start -d
15
+ ```
16
+
17
+ Kill daemonized:
18
+ ```bash
19
+ kill `cat [daemon_name].pid`
20
+ ```
21
+
22
+ ### Console
23
+ `bin/console`
@@ -0,0 +1,13 @@
1
+ require 'active_record'
2
+ require 'active_record/migrations/tasks'
3
+ ActiveRecord::Migrations.root = '.'
4
+ require File.expand_path(File.join('..', 'config', 'application.rb'), __FILE__)
5
+
6
+ <%= camelized_daemon_name %>.app.setup
7
+
8
+ DATABASE_ENV = <%= camelized_daemon_name %>.app.env
9
+
10
+ task :environment do
11
+ end
12
+
13
+ Dir["#{<%= camelized_daemon_name %>.root}/lib/tasks/*.rake"].each { |rake_file| load rake_file }
@@ -0,0 +1,14 @@
1
+ # Uncomment the following code to have the Clockwork integration running in the daemon as a separate thread.
2
+ # https://github.com/Rykian/clockwork
3
+
4
+ # require 'clockwork'
5
+ #
6
+ # module Clockwork
7
+ # configure do |config|
8
+ # config[:logger] = <%= camelized_daemon_name %>.app.logger
9
+ # end
10
+ #
11
+ # every(1.hour, 'label for logger') { Feed.send_later(:refresh) }
12
+ # every(10.seconds, 'label for logger') { run_something }
13
+ # every(1.day, 'label for logger', at: '01:30') { run_daily_processing }
14
+ # end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,13 @@
1
+ # A dummy SubprocessLoop that logs Hello World every 10 seconds, offseted by 5 seconds
2
+ class FooSubprocessLoop < Archfiend::SubprocessLoop
3
+ def run
4
+ sleep 5
5
+ super
6
+ end
7
+
8
+ def iterate
9
+ msg = "Hello World from #{self.class.name}"
10
+ logger.info(msg)
11
+ sleep 10
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ # A dummy thread loop that logs Hello World every 5 seconds
2
+ class BarThreadLoop < Archfiend::ThreadLoop
3
+ def iterate
4
+ msg = "Hello World from #{self.class.name}"
5
+ logger.info(msg)
6
+ sleep 5
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ bundle exec pry -r ./config/environment.rb
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ if [ "$1" == "-d" ]; then
3
+ ruby ./config/daemon.rb
4
+ else
5
+ ruby -r ./config/application.rb -e "<%= camelized_daemon_name %>.app.run"
6
+ fi
@@ -0,0 +1,27 @@
1
+ require File.expand_path('boot', __dir__)
2
+ require 'archfiend'
3
+
4
+ # Your daemon namespace
5
+ module <%= camelized_daemon_name %>
6
+ extend Archfiend::Utilities
7
+
8
+ # Main daemon application class
9
+ class Application < Archfiend::Application
10
+ # @return [Module] A module containing Archfiend::Utilities
11
+ def utils
12
+ ::<%= camelized_daemon_name %>
13
+ end
14
+ end
15
+
16
+ # @return [Pathname] An absolute path to the main directory of the daemon
17
+ def self.root
18
+ @root ||= Pathname.new(File.expand_path('..', __dir__))
19
+ end
20
+ end
21
+
22
+ Bundler.require(*<%= camelized_daemon_name %>.groups)
23
+
24
+ silence_warnings do
25
+ Encoding.default_external = Encoding::UTF_8
26
+ Encoding.default_internal = Encoding::UTF_8
27
+ end
@@ -0,0 +1,3 @@
1
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2
+
3
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
@@ -0,0 +1,18 @@
1
+ require File.expand_path('application', __dir__)
2
+
3
+ daemon_opts = {
4
+ app_name: '<%= daemon_name %>',
5
+ log_dir: File.join(File.expand_path(__dir__), 'log'),
6
+ logfilename: "#{<%= camelized_daemon_name %>.app.env}.log"
7
+ }
8
+
9
+ # Daemons.daemonize closes all file descriptors and prepares the app for the fork.
10
+ # Make sure new code doesn't rely on connections / logs etc opened before this call
11
+ Daemons.daemonize(daemon_opts)
12
+
13
+ # Sets up Settings and many more
14
+ <%= camelized_daemon_name %>.app.setup
15
+ Process.setproctitle "#{Settings.app_name} (#{<%= camelized_daemon_name %>.app.env})"
16
+
17
+ # Runs the actual application
18
+ <%= camelized_daemon_name %>.app.run
@@ -0,0 +1,26 @@
1
+ default: &default
2
+ adapter: postgresql
3
+ encoding: utf8
4
+ database: <%%= ENV.fetch('DATABASE_NAME', '<%= daemon_name %>_development') %>
5
+
6
+ username: <%%= ENV.fetch('DATABASE_USERNAME', 'username') %>
7
+ password: <%%= ENV.fetch('DATABASE_PASSWORD', 'password') %>
8
+
9
+ host: <%%= ENV.fetch('DATABASE_HOST', 'localhost') %>
10
+ port: <%%= ENV.fetch('DATABASE_PORT', 5432) %>
11
+
12
+ pool: 12
13
+ reconnect: true
14
+
15
+ development:
16
+ <<: *default
17
+
18
+ test:
19
+ <<: *default
20
+ database: <%= daemon_name %>_test
21
+
22
+ staging:
23
+ <<: *default
24
+
25
+ production:
26
+ <<: *default
@@ -0,0 +1,4 @@
1
+ # File required by spec_helper and by console. Loads the basic environment of the Producer
2
+ require File.expand_path('application', __dir__)
3
+
4
+ <%= camelized_daemon_name %>.app.setup
@@ -0,0 +1,8 @@
1
+ app_name: <%= camelized_daemon_name %>
2
+
3
+ logger:
4
+ # :debug, :info, :error, :fatal, :unknown
5
+ level: :info
6
+
7
+ debug:
8
+ rbtrace: false
@@ -0,0 +1,5 @@
1
+ logger:
2
+ level: :info
3
+
4
+ debug:
5
+ rbtrace: true
@@ -0,0 +1,43 @@
1
+ ENV['APP_ENV'] = 'test'
2
+
3
+ require File.expand_path('../config/environment', __dir__)
4
+ require 'database_cleaner'
5
+ require 'support/timecop'
6
+ require 'support/factory_bot'
7
+
8
+ RSpec.configure do |config|
9
+ # Enable flags like --only-failures and --next-failure
10
+ config.example_status_persistence_file_path = '.rspec_status'
11
+
12
+ # Disable RSpec exposing methods globally on `Module` and `main`
13
+ config.disable_monkey_patching!
14
+
15
+ config.expect_with :rspec do |c|
16
+ c.syntax = :expect
17
+ c.include_chain_clauses_in_custom_matcher_descriptions = true
18
+ end
19
+
20
+ config.mock_with :rspec do |mocks|
21
+ # Prevents you from mocking or stubbing a method that does not exist on
22
+ # a real object. This is generally recommended, and will default to
23
+ # `true` in RSpec 4.
24
+ mocks.verify_partial_doubles = true
25
+ end
26
+
27
+ config.around do |example|
28
+ ActiveRecord::Base.logger = nil
29
+ DatabaseCleaner.strategy = :truncation
30
+ DatabaseCleaner.cleaning { example.run }
31
+ end
32
+
33
+ config.include(Shoulda::Matchers::ActiveModel, type: :model)
34
+ config.include(Shoulda::Matchers::ActiveRecord, type: :model)
35
+ end
36
+
37
+ Shoulda::Matchers.configure do |config|
38
+ config.integrate do |with|
39
+ with.test_framework :rspec
40
+ with.library :active_record
41
+ with.library :active_model
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ RSpec.describe FooSubprocessLoop do
2
+ it 'has iterate method' do
3
+ expect(subject).to respond_to(:iterate)
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ require 'factory_bot'
2
+
3
+ RSpec.configure do |config|
4
+ config.include FactoryBot::Syntax::Methods
5
+
6
+ config.before(:suite) do
7
+ FactoryBot.find_definitions
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'timecop'
2
+
3
+ RSpec.configure do |config|
4
+ config.around(:each, freeze: true) do |example|
5
+ # Default is current time rounded to seconds
6
+ time = Time.zone.at(Time.now.to_i)
7
+ Timecop.freeze(time) { example.run }
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ RSpec.describe BarThreadLoop do
2
+ it 'has iterate method' do
3
+ expect(subject).to respond_to(:iterate)
4
+ end
5
+ end
@@ -0,0 +1,119 @@
1
+ module Archfiend
2
+ module Generators
3
+ class Extensions
4
+ PHASES = %i[init exec].freeze
5
+ CALLBACK_TYPES = %i[before after].freeze
6
+
7
+ # @param generator_options [Archfiend::Generators::Options] Options, source of potential extensions
8
+ # @param action_context [Thor::Group] Contex of the extended action/group of actions
9
+ # @param generator_name [String] Underscore form name of the generator, ex. daemon
10
+ def initialize(generator_options, action_context, generator_name)
11
+ @generator_options = generator_options
12
+ @action_context = action_context
13
+ @generator_name = generator_name
14
+
15
+ @extensions = activate_extensions
16
+ expose_extensions
17
+ end
18
+
19
+ def run_with_init_callbacks
20
+ run_callback(:init, :before)
21
+
22
+ yield unless skip_default_action?(:init)
23
+
24
+ run_callback(:init, :after)
25
+ end
26
+
27
+ def run_with_exec_callbacks
28
+ run_callback(:exec, :before)
29
+
30
+ yield unless skip_default_action?(:exec)
31
+
32
+ run_callback(:exec, :after)
33
+ end
34
+
35
+ private
36
+
37
+ def skip_default_action?(phase)
38
+ generator_extensions.any? do |ge|
39
+ method_name = "skip_default_#{phase}_action?"
40
+ ge.respond_to?(method_name) && ge.public_send(method_name)
41
+ end
42
+ end
43
+
44
+ def run_callback(phase, callback_type)
45
+ fail(ArgumentError, "Unsupported phase #{phase}") unless PHASES.include?(phase)
46
+ fail(ArgumentError, "Unsupported callback_type #{callback_type}") unless CALLBACK_TYPES.include?(callback_type)
47
+
48
+ callback_action = [callback_type, phase].join('_')
49
+ generator_extensions.select { |ge| ge.respond_to?(callback_action) }.each { |ge| ge.public_send(callback_action) }
50
+ end
51
+
52
+ def run_before_create_extensions
53
+ generator_extensions.select { |ge| ge.respond_to?(:before_create) }.each(&:before_create)
54
+ end
55
+
56
+ def run_after_create_extensions
57
+ generator_extensions.select { |ge| ge.respond_to?(:after_create) }.each(&:after_create)
58
+ end
59
+
60
+ def generator_extensions
61
+ @generator_extensions ||= @extensions.map do |extension_module|
62
+ next unless extension_module.const_defined?(generator_extensions_class_name)
63
+ extension_klass = extension_module.const_get(generator_extensions_class_name)
64
+ next if extension_klass.respond_to?(:target_generator_name) && extension_klass.target_generator_name != @generator_name
65
+
66
+ extension_klass.new(@action_context, @generator_options)
67
+ end.compact
68
+ end
69
+
70
+ def generator_extensions_class_name
71
+ "Generators::#{@generator_name.camelize}Extensions" # example: Generators::DaemonExtensions
72
+ end
73
+
74
+ def expose_extensions # rubocop:disable Metrics/AbcSize
75
+ generator_extensions.each do |generator_extension|
76
+ exposed_name = if generator_extension.respond_to?(:exposed_name)
77
+ generator_extension.exposed_name
78
+ else
79
+ generator_extension.class.name.split(':').first.underscore
80
+ end
81
+
82
+ if @action_context.respond_to?(exposed_name)
83
+ puts "Extension's exposed_name #{exposed_name.inspect} conflicts with existing method defined in #{@action_context.method(exposed_name).source_location}."
84
+ puts "Please define a method #{generator_extension.class.name}#exposed_name with some other value."
85
+ exit 1
86
+ end
87
+
88
+ @action_context.instance_variable_set("@#{exposed_name}", generator_extension)
89
+ @action_context.class.attr_reader(exposed_name)
90
+ end
91
+ end
92
+
93
+ def activate_extensions # rubocop:disable Metrics/AbcSize
94
+ extensions = []
95
+
96
+ @generator_options.extensions.each do |extension|
97
+ begin
98
+ Kernel.gem(extension)
99
+ Kernel.require(extension)
100
+ rescue LoadError => e
101
+ puts "Unable to load requested extension gem #{extension}, aborting"
102
+ puts e.inspect.to_s
103
+ exit 1
104
+ end
105
+ module_name = extension.camelize
106
+ if Object.const_defined?(module_name)
107
+ extensions << Object.const_get(module_name)
108
+ puts "Activated extension gem #{module_name}"
109
+ else
110
+ puts "Failed to recognize extension module #{module_name} in gem #{extension}, aborting"
111
+ exit 1
112
+ end
113
+ end
114
+
115
+ extensions
116
+ end
117
+ end
118
+ end
119
+ end