archfiend 0.1.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 (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