fiber_job 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a6c46d9a9278acfb132e182abe3f176e96e1b948aeaee30a722793b4b47b222
4
- data.tar.gz: 70d45d9ee91164f8a5016bfc5cbae46dd4250de0a45d6658f442f4c4c78a72e2
3
+ metadata.gz: 902c8f2047330d092fdcadda50067f8bef6a47bc43039ed6bad7172d6976a1b7
4
+ data.tar.gz: 9cd001ba179f3f6a16619441b448e1d46dc7917431a3deea753615135af499f7
5
5
  SHA512:
6
- metadata.gz: f2a203f5d2124903dff417ca2562862a3326e496037e8d56e542629cbabbbfe1a129c9888bc31b047d522617db2afb18de22ec112d2c0ec9b37525f48046aedf
7
- data.tar.gz: fc605f346003929db83d530b44024cc2f402c503041d1bb8ef890281c5762c581b943f628b289f4bcd5437a5391607c73db0a80c76391ff1de13d7cc5609220f
6
+ metadata.gz: 391d2e8826377526d5c852f1ba1a07a84767d3c57544d73ce93cb7cb6e3f3cf381591e6a76049219d3122259af047d6f3dce492a00aea764319f53583ba20385
7
+ data.tar.gz: e5dce3e93ae978c1c0d02169cfe2b95cd92888a92d9f35675f372cd31fe6ae17e2a6a551a977abaed40095e2d5de3068dc94cbe59b69c0a01ef54ed005b46ab8
data/README.md CHANGED
@@ -2,24 +2,10 @@
2
2
 
3
3
  A high-performance, Redis-based background job processing library for Ruby built on modern fiber-based concurrency. FiberJob combines the persistence of Redis with the speed of async fibers to deliver exceptional performance and reliability.
4
4
 
5
- ## Architecture Highlights
6
-
7
- FiberJob is a experimental gem that uses a architecture that sets it apart from traditional job queues:
8
-
9
- ### **Hybrid Redis + Async::Queue Design**
10
- - **Redis for persistence**: Durable job storage with atomic operations and scheduling
11
- - **Async::Queue for speed**: Lightning-fast in-memory job processing with fiber-based concurrency
12
- - **Best of both worlds**: Reliability of Redis + performance of in-memory queues
13
-
14
- ### **Advanced Fiber Management**
15
- - **Separation of concerns**: Independent polling fibers fetch from Redis while processing fibers execute jobs
16
- - **Per-queue fiber pools**: Isolated concurrency control with `Async::Semaphore` for optimal resource utilization
17
- - **Non-blocking operations**: All I/O operations use async/await patterns for maximum throughput
5
+ ## Requirements
18
6
 
19
- ### **Production-Optimized Performance**
20
- - **Minimal Redis contention**: Single polling fiber per queue reduces Redis load
21
- - **Fast job execution**: Jobs flow through in-memory `Async::Queue` for sub-millisecond processing
22
- - **Scalable concurrency**: Configurable fiber pools scale efficiently without thread overhead
7
+ - Ruby 3.1+
8
+ - Redis 5.0+
23
9
 
24
10
  ## Features
25
11
 
@@ -266,11 +252,6 @@ end
266
252
  - `REDIS_URL`: Redis connection URL
267
253
  - `FIBER_JOB_LOG_LEVEL`: Logging level
268
254
 
269
- ## Requirements
270
-
271
- - Ruby 3.1+
272
- - Redis 5.0+
273
-
274
255
  ## License
275
256
 
276
257
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
data/bin/fiber_job CHANGED
@@ -2,29 +2,71 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative '../lib/fiber_job'
5
+ require 'optparse'
5
6
 
6
- # Simple CLI for FiberJob
7
- command = ARGV[0]
7
+ # CLI for FiberJob with configuration file support
8
+ def load_configuration_file(config_path)
9
+ unless File.exist?(config_path)
10
+ puts "Error: Configuration file not found: #{config_path}"
11
+ exit 1
12
+ end
8
13
 
9
- case command
10
- when 'worker'
11
- puts "Starting FiberJob worker..."
12
- FiberJob::ProcessManager.start_worker
13
- when 'version'
14
- puts "FiberJob version #{FiberJob::VERSION}"
15
- when nil, 'help'
14
+ begin
15
+ load config_path
16
+ rescue => e
17
+ puts "Error loading configuration file: #{e.message}"
18
+ exit 1
19
+ end
20
+ end
21
+
22
+ def show_help
16
23
  puts <<~HELP
17
24
  FiberJob - High-performance fiber-based job processing
18
25
 
19
26
  Usage:
20
- fiber_job worker Start a worker process
27
+ fiber_job start [options] Start the worker server
21
28
  fiber_job version Show version
22
29
  fiber_job help Show this help
23
30
 
31
+ Options:
32
+ -c, --config FILE Path to configuration file
33
+
24
34
  Examples:
25
- fiber_job worker # Start worker with default configuration
26
- REDIS_URL=redis://localhost:6379/1 fiber_job worker # Custom Redis URL
35
+ fiber_job start # Start with default configuration
36
+ fiber_job start -c config.rb # Start with custom configuration
37
+ REDIS_URL=redis://localhost:6379/1 fiber_job start # Environment override
27
38
  HELP
39
+ end
40
+
41
+ # Parse command and options
42
+ command = ARGV[0]
43
+ config_file = nil
44
+
45
+ case command
46
+ when 'start'
47
+ # Parse options for start command
48
+ OptionParser.new do |opts|
49
+ opts.on('-c', '--config FILE', 'Configuration file path') do |file|
50
+ config_file = file
51
+ end
52
+ opts.on('-h', '--help', 'Show help') do
53
+ show_help
54
+ exit
55
+ end
56
+ end.parse!(ARGV[1..-1])
57
+
58
+ # Load configuration file if specified
59
+ load_configuration_file(config_file) if config_file
60
+
61
+ puts "Starting FiberJob server..."
62
+ FiberJob::ProcessManager.start_worker
63
+
64
+ when 'version'
65
+ puts "FiberJob version #{FiberJob::VERSION}"
66
+
67
+ when nil, 'help', '-h', '--help'
68
+ show_help
69
+
28
70
  else
29
71
  puts "Unknown command: #{command}"
30
72
  puts "Run 'fiber_job help' for usage information"
@@ -39,8 +39,6 @@ module FiberJob
39
39
 
40
40
  queue_name = job_class.queue
41
41
  Queue.push(queue_name, payload)
42
-
43
- FiberJob.logger.info "Enqueued #{job_class.name} with args: #{args.inspect}"
44
42
  end
45
43
 
46
44
  # Enqueues a job for execution after a specified delay.
@@ -4,15 +4,56 @@ require 'async'
4
4
  require 'async/semaphore'
5
5
 
6
6
  module FiberJob
7
- # The ConcurrencyManager class is a lightweight wrapper around Async::Semaphore that
8
- # controls how many jobs can execute simultaneously within each queue
7
+ # The ConcurrencyManager class provides efficient fiber reuse through direct
8
+ # semaphore acquire/release operations instead of spawning new fibers
9
9
  class ConcurrencyManager
10
+ attr_reader :semaphore
11
+
10
12
  def initialize(max_concurrency: 5)
11
13
  @semaphore = Async::Semaphore.new(max_concurrency)
12
14
  end
13
15
 
14
- def execute(&block)
15
- @semaphore.async(&block)
16
+ # Acquire a semaphore permit, blocking if none available
17
+ # @return [void]
18
+ def acquire
19
+ @semaphore.acquire
20
+ end
21
+
22
+ # Release a semaphore permit, allowing waiting fibers to proceed
23
+ # @return [void]
24
+ def release
25
+ @semaphore.release
26
+ end
27
+
28
+ # Execute a block with automatic acquire/release handling
29
+ # This method reuses the current fiber instead of spawning new ones
30
+ # @yield [void] Block to execute within semaphore protection
31
+ # @return [Object] Result of the block execution
32
+ def execute(&)
33
+ acquire
34
+ begin
35
+ yield
36
+ ensure
37
+ release
38
+ end
39
+ end
40
+
41
+ # Check if the semaphore would block on acquire
42
+ # @return [Boolean] true if acquire would block
43
+ def blocking?
44
+ @semaphore.blocking?
45
+ end
46
+
47
+ # Current number of acquired permits
48
+ # @return [Integer] Number of active permits
49
+ def count
50
+ @semaphore.count
51
+ end
52
+
53
+ # Maximum number of permits
54
+ # @return [Integer] Semaphore limit
55
+ def limit
56
+ @semaphore.limit
16
57
  end
17
58
  end
18
59
  end
@@ -24,6 +24,11 @@ module FiberJob
24
24
  # }
25
25
  # end
26
26
  #
27
+ # @example Job auto-loading
28
+ # FiberJob.configure do |config|
29
+ # config.job_paths = ['app/jobs', 'lib/jobs']
30
+ # end
31
+ #
27
32
  class Config
28
33
  # @!attribute [rw] redis_url
29
34
  # @return [String] Redis connection URL
@@ -37,7 +42,9 @@ module FiberJob
37
42
  # @return [Logger] Logger instance for application logging
38
43
  # @!attribute [rw] log_level
39
44
  # @return [Symbol] Logging level (:debug, :info, :warn, :error)
40
- attr_accessor :redis_url, :concurrency, :queues, :queue_concurrency, :logger, :log_level
45
+ # @!attribute [rw] job_paths
46
+ # @return [Array<String>] List of paths to auto-load job classes from
47
+ attr_accessor :redis_url, :concurrency, :queues, :queue_concurrency, :logger, :log_level, :job_paths
41
48
 
42
49
  # Initializes configuration with sensible defaults.
43
50
  # Values can be overridden through environment variables or configuration blocks.
@@ -49,12 +56,13 @@ module FiberJob
49
56
  # - FIBER_JOB_LOG_LEVEL: Logging level (default: info)
50
57
  def initialize
51
58
  @redis_url = ENV['REDIS_URL'] || 'redis://localhost:6379'
52
- @concurrency = 2 # Global default fallback
59
+ @concurrency = 2
53
60
  @queues = [:default]
54
61
  @queue_concurrency = { default: 2 } # Per-queue concurrency
55
62
  @log_level = ENV['FIBER_JOB_LOG_LEVEL']&.to_sym || :info
56
63
  @logger = ::Logger.new($stdout)
57
64
  @logger.level = ::Logger.const_get(@log_level.to_s.upcase)
65
+ @job_paths = []
58
66
  end
59
67
 
60
68
  # Returns the concurrency setting for a specific queue.
@@ -70,5 +78,62 @@ module FiberJob
70
78
  def concurrency_for_queue(queue_name)
71
79
  @queue_concurrency[queue_name.to_sym] || @concurrency
72
80
  end
81
+
82
+ # Auto-loads job classes from configured paths.
83
+ # Recursively loads all .rb files in the specified directories
84
+ # and validates that they contain classes inheriting from FiberJob::Job.
85
+ #
86
+ # @return [Array<Class>] List of loaded job classes
87
+ #
88
+ # @example Auto-load jobs
89
+ # config.job_paths = ['app/jobs', 'lib/jobs']
90
+ # loaded_jobs = config.load_jobs!
91
+ # # => [EmailJob, DataProcessingJob, ...]
92
+ def load_jobs!
93
+ loaded_classes = []
94
+
95
+ @job_paths.each do |path|
96
+ unless Dir.exist?(path)
97
+ @logger.warn "Job path does not exist: #{path}"
98
+ next
99
+ end
100
+
101
+ @logger.info "Loading jobs from: #{path}"
102
+
103
+ Dir.glob("#{path}/**/*.rb").sort.each do |file|
104
+ begin
105
+ # Track classes before requiring the file
106
+ classes_before = job_classes
107
+
108
+ require_relative File.expand_path(file)
109
+
110
+ # Find newly loaded job classes
111
+ new_classes = job_classes - classes_before
112
+
113
+ new_classes.each do |job_class|
114
+ @logger.debug "Loaded job class: #{job_class}"
115
+ loaded_classes << job_class
116
+ end
117
+
118
+ rescue => e
119
+ @logger.error "Failed to load job file #{file}: #{e.message}"
120
+ end
121
+ end
122
+ end
123
+
124
+ @logger.info "Loaded #{loaded_classes.size} job classes"
125
+ loaded_classes
126
+ end
127
+
128
+ private
129
+
130
+ # Returns all classes that inherit from FiberJob::Job
131
+ def job_classes
132
+ return [] unless defined?(FiberJob::Job)
133
+
134
+ ObjectSpace.each_object(Class).select do |klass|
135
+ klass < FiberJob::Job
136
+ end
137
+ end
73
138
  end
74
139
  end
data/lib/fiber_job/job.rb CHANGED
@@ -43,13 +43,18 @@ module FiberJob
43
43
  # @return [Integer] Job priority (higher numbers = higher priority)
44
44
  # @!attribute [rw] timeout
45
45
  # @return [Integer] Maximum execution time in seconds before timeout
46
+ # @!attribute [r] config
47
+ # @return [FiberJob::Config] Configuration object for this job instance
46
48
  attr_accessor :queue, :retry_count, :max_retries, :priority, :timeout
49
+ attr_reader :config
47
50
 
48
51
  # Initializes a new job instance with default configuration.
49
- # Sets reasonable defaults for queue, retries, and timeout values.
52
+ # Uses centralized configuration for defaults instead of hardcoded values.
50
53
  #
54
+ # @param config [FiberJob::Config, nil] Configuration object to use for defaults
51
55
  # @return [void]
52
- def initialize
56
+ def initialize(config: nil)
57
+ @config = config || FiberJob.config
53
58
  @queue = :default
54
59
  @retry_count = 0
55
60
  @max_retries = 3
@@ -1,18 +1,94 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FiberJob
4
- # ProcessManager is responsible for managing the lifecycle of workers
5
- # and ensuring they run with the correct configuration.
4
+ # ProcessManager is the single entry point for running FiberJob workers.
5
+ # It manages worker lifecycle, configuration, signal handling, and graceful shutdown.
6
+ #
7
+ # This is the recommended way to start FiberJob workers and should be used
8
+ # instead of directly instantiating Worker objects.
9
+ #
10
+ # @example Start with default configuration
11
+ # FiberJob::ProcessManager.start_worker
12
+ #
13
+ # @example Start with custom configuration
14
+ # config = FiberJob::Config.new
15
+ # config.concurrency = 10
16
+ # FiberJob::ProcessManager.start_worker(config: config)
17
+ #
6
18
  class ProcessManager
7
- def self.start_worker(queues: nil, concurrency: nil)
8
- queues ||= FiberJob.config.queues
9
- concurrency ||= FiberJob.config.concurrency
10
- worker = Worker.new(queues: queues, concurrency: concurrency)
19
+ class << self
20
+ # Starts a FiberJob worker process with proper signal handling and lifecycle management.
21
+ # This is the main entry point for running FiberJob workers.
22
+ #
23
+ # @param config [FiberJob::Config, nil] Configuration object (defaults to FiberJob.config)
24
+ # @param queues [Array<Symbol>, nil] Queue names to process (defaults to config.queues)
25
+ # @param concurrency [Integer, nil] Concurrency level (defaults to config.concurrency)
26
+ # @return [void]
27
+ #
28
+ # @example Basic usage
29
+ # FiberJob::ProcessManager.start_worker
30
+ #
31
+ # @example With custom configuration
32
+ # FiberJob::ProcessManager.start_worker(
33
+ # config: my_config,
34
+ # queues: [:high_priority, :default],
35
+ # concurrency: 8
36
+ # )
37
+ def start_worker(config: nil, queues: nil, concurrency: nil)
38
+ config ||= FiberJob.config
39
+ queues ||= config.queues
40
+ concurrency ||= config.concurrency
11
41
 
12
- trap('INT') { worker.stop }
13
- trap('TERM') { worker.stop }
42
+ # Auto-load job classes if paths are configured
43
+ config.load_jobs! if config.job_paths&.any?
44
+
45
+ log_startup_info(config, queues, concurrency)
46
+
47
+ worker = create_worker(config: config, queues: queues, concurrency: concurrency)
48
+ setup_signal_handlers(worker)
49
+
50
+ begin
51
+ worker.start
52
+ rescue => e
53
+ config.logger.error "Failed to start FiberJob worker: #{e.message}"
54
+ config.logger.error e.backtrace.join("\n")
55
+ exit 1
56
+ end
57
+ end
14
58
 
15
- worker.start
59
+ private
60
+
61
+ # Creates and configures a new worker instance
62
+ def create_worker(config:, queues:, concurrency:)
63
+ Worker.new(config: config, queues: queues, concurrency: concurrency)
64
+ end
65
+
66
+ # Sets up signal handlers for graceful shutdown
67
+ def setup_signal_handlers(worker)
68
+ %w[INT TERM].each do |signal|
69
+ trap(signal) do
70
+ worker.config.logger.info "Received #{signal}, shutting down gracefully..."
71
+ worker.stop
72
+ end
73
+ end
74
+ end
75
+
76
+ # Logs startup information for debugging and monitoring
77
+ def log_startup_info(config, queues, concurrency)
78
+ logger = config.logger
79
+ logger.info "Starting FiberJob worker server"
80
+ logger.info "Redis URL: #{config.redis_url}"
81
+ logger.info "Queues: #{queues.join(', ')}"
82
+ logger.info "Global concurrency: #{concurrency}"
83
+
84
+ queues.each do |queue|
85
+ queue_concurrency = config.concurrency_for_queue(queue)
86
+ worker_fibers = [queue_concurrency, 10].min
87
+ logger.info "Queue '#{queue}' concurrency: #{queue_concurrency} (#{worker_fibers} worker fibers)"
88
+ end
89
+
90
+ logger.info "Log level: #{config.log_level}"
91
+ end
16
92
  end
17
93
  end
18
94
  end
@@ -27,12 +27,20 @@ module FiberJob
27
27
  #
28
28
  # @see FiberJob::Worker
29
29
  class Queue
30
+ class << self
31
+ attr_writer :config
32
+
33
+ def config
34
+ @config ||= FiberJob.config
35
+ end
36
+ end
37
+
30
38
  # Returns the shared Redis connection instance.
31
39
  # Creates a new connection if one doesn't exist.
32
40
  #
33
41
  # @return [Redis] The shared Redis connection
34
42
  def self.redis
35
- @redis ||= Redis.new(url: FiberJob.config.redis_url)
43
+ @redis ||= Redis.new(url: config.redis_url)
36
44
  end
37
45
 
38
46
  # Creates a new Redis connection for fiber-safe operations.
@@ -41,7 +49,7 @@ module FiberJob
41
49
  # @return [Redis] A new Redis connection instance
42
50
  def self.redis_connection
43
51
  # Create a new Redis connection for fiber-safe operations
44
- Redis.new(url: FiberJob.config.redis_url)
52
+ Redis.new(url: config.redis_url)
45
53
  end
46
54
 
47
55
  # Adds a job to the specified queue for immediate processing.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FiberJob
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -4,13 +4,26 @@ require 'async'
4
4
  require 'async/queue'
5
5
 
6
6
  module FiberJob
7
+ # Worker handles the actual job processing using fiber-based concurrency.
8
+ #
9
+ # @private
10
+ # This class is intended for internal use by ProcessManager and should not
11
+ # be instantiated directly. Use FiberJob::ProcessManager.start_worker instead.
12
+ #
13
+ # @see FiberJob::ProcessManager
7
14
  class Worker
8
- def initialize(queues: nil, concurrency: nil)
9
- @queues = queues || FiberJob.config.queues
10
- @concurrency = concurrency || FiberJob.config.concurrency
15
+ attr_reader :config
16
+
17
+ def initialize(config: nil, queues: nil, concurrency: nil)
18
+ @config = config || FiberJob.config
19
+ @queues = queues || @config.queues
20
+ @concurrency = concurrency || @config.concurrency
11
21
  @running = false
12
22
  @managers = {}
13
23
  @job_queues = {} # In-memory Async::Queue per Redis queue
24
+
25
+ # Inject configuration into Queue class
26
+ Queue.config = @config
14
27
  end
15
28
 
16
29
  def start
@@ -20,20 +33,19 @@ module FiberJob
20
33
  # Initialize all queues first
21
34
  @queues.each do |queue_name|
22
35
  @job_queues[queue_name] = Async::Queue.new
23
- queue_concurrency = FiberJob.config.concurrency_for_queue(queue_name)
36
+ queue_concurrency = @config.concurrency_for_queue(queue_name)
24
37
  @managers[queue_name] = ConcurrencyManager.new(max_concurrency: queue_concurrency)
25
38
  end
26
39
 
27
- # Start independent pollers for each queue
28
40
  @queues.each do |queue_name|
29
41
  task.async do
30
42
  poll_redis_queue(queue_name)
31
43
  end
32
44
  end
33
45
 
34
- # Start independent worker pools for each queue
46
+ # Start fixed worker fiber pools for efficient fiber reuse
35
47
  @queues.each do |queue_name|
36
- queue_concurrency = FiberJob.config.concurrency_for_queue(queue_name)
48
+ queue_concurrency = @config.concurrency_for_queue(queue_name) || @concurrency
37
49
  queue_concurrency.times do
38
50
  task.async do
39
51
  process_job_queue(queue_name)
@@ -41,7 +53,6 @@ module FiberJob
41
53
  end
42
54
  end
43
55
 
44
- # Global support fibers
45
56
  task.async do
46
57
  process_scheduled_jobs
47
58
  end
@@ -62,36 +73,31 @@ module FiberJob
62
73
  private
63
74
 
64
75
  # Single poller fiber that fetches jobs from Redis and distributes to workers
65
- # This eliminates Redis brpop contention by having only one fiber per queue accessing Redis
66
76
  def poll_redis_queue(queue_name)
67
- # Create dedicated Redis connection for this poller to avoid blocking other pollers
68
77
  redis_conn = Queue.redis_connection
69
78
  while @running
70
79
  begin
71
- # Use longer timeout since we're the only poller - no contention
72
80
  job_data = Queue.pop(queue_name, timeout: 1.0, redis_conn: redis_conn)
73
81
 
74
82
  if job_data
75
- # Push to in-memory queue for worker fibers to process
76
83
  @job_queues[queue_name].push(job_data)
77
84
  end
78
85
  rescue StandardError => e
79
86
  FiberJob.logger.error "Redis polling error for queue #{queue_name}: #{e.message}"
80
- sleep(1) # Brief pause on error
87
+ sleep(1)
81
88
  end
82
89
  end
83
90
  end
84
91
 
85
92
  # Worker fibers process jobs from the fast in-memory queue
86
- # Multiple workers can process concurrently without Redis contention
93
+ # Each fiber reuses itself by acquiring/releasing semaphore permits
87
94
  def process_job_queue(queue_name)
88
95
  while @running
89
96
  begin
90
- # Fast in-memory dequeue operation
91
97
  job_data = @job_queues[queue_name].dequeue
92
98
 
93
99
  if job_data
94
- # Use semaphore to control actual job execution concurrency
100
+ # Reuse current fiber with semaphore-controlled concurrency
95
101
  @managers[queue_name].execute do
96
102
  execute_job(job_data)
97
103
  end
@@ -111,10 +117,6 @@ module FiberJob
111
117
 
112
118
  job.retry_count = job_data['retry_count'] || 0
113
119
 
114
- # if job.retry_count > 0
115
- # FiberJob.logger.info "Executing #{job_class} (retry #{job.retry_count}/#{job.max_retries})"
116
- # end
117
-
118
120
  begin
119
121
  Timeout.timeout(job.timeout) do
120
122
  args = (job_data['args'] || []).dup
data/lib/fiber_job.rb CHANGED
@@ -105,4 +105,4 @@ module FiberJob
105
105
  end
106
106
 
107
107
  # Register cron jobs after module is fully loaded
108
- FiberJob.register_cron_jobs
108
+ FiberJob.register_cron_jobs
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fiber_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Caio Mendonca