Hot Reload
Hot reload automatically reloads your bot code when files change, without restarting the bot. This dramatically speeds up development.
How Hot Reload Works
File Changed ──► Detect Change ──► Reload Code ──► Apply Updates
↑ │
└───────────────────────────────────────────────────────┘
Hot reload:
- Watches files for changes
- Reloads changed code
- Updates running bot
- Preserves connections and state
Enabling Hot Reload
Basic Setup
bot = DiscordRDA::Bot.new(token: ENV['DISCORD_TOKEN'])
# Enable hot reload
bot.enable_hot_reload(watch_dir: 'lib')
bot.run
With Options
bot.enable_hot_reload(
watch_dir: 'lib', # Directory to watch
extensions: ['.rb'], # File extensions
ignore: ['spec/', 'tmp/'], # Patterns to ignore
debounce: 1.0, # Seconds to wait after change
verbose: true # Log reloads
)
What Gets Reloaded
Command Files
# lib/commands/ping.rb
bot.slash('ping', 'Check latency') do |cmd|
cmd.handler do |interaction|
interaction.respond(content: 'Pong! v2') # Change this
end
end
When you save, the command is updated immediately.
Event Handlers
# lib/events/message.rb
bot.on(:message_create) do |event|
# Modify this handler
if event.content == '!test'
event.message.respond(content: 'Updated!')
end
end
Plugins
# lib/plugins/my_plugin.rb
class MyPlugin < DiscordRDA::Plugin
def ready(bot)
puts "Plugin v3 ready!" # Change version
end
end
Project Structure for Hot Reload
Recommended structure:
my_bot/
├── bot.rb # Entry point (don't reload this)
├── lib/
│ ├── commands/ # Command definitions
│ │ ├── ping.rb
│ │ └── info.rb
│ ├── events/ # Event handlers
│ │ ├── ready.rb
│ │ └── message.rb
│ └── plugins/ # Plugin definitions
│ └── my_plugin.rb
└── config/
└── bot_config.rb
Entry Point (bot.rb)
require 'discord_rda'
bot = DiscordRDA::Bot.new(
token: ENV['DISCORD_TOKEN'],
application_id: ENV['DISCORD_APP_ID'],
intents: [:guilds, :guild_messages]
)
# Load all commands and handlers
Dir['lib/commands/*.rb'].each { |f| load f }
Dir['lib/events/*.rb'].each { |f| load f }
Dir['lib/plugins/*.rb'].each { |f| load f }
# Enable hot reload
bot.enable_hot_reload(watch_dir: 'lib')
bot.run
Reload Strategies
Full Reload
Reload entire files (default):
bot.enable_hot_reload(
watch_dir: 'lib',
strategy: :full # Reload entire file
)
Selective Reload
Only reload changed methods (experimental):
bot.enable_hot_reload(
watch_dir: 'lib',
strategy: :selective
)
Handling State
Preserving State Across Reloads
# Use a singleton or global for state
module BotState
class << self
attr_accessor :message_count, :user_cache
end
self.message_count = 0
self.user_cache = {}
end
# lib/events/message.rb
bot.on(:message_create) do |event|
BotState.message_count += 1
puts "Messages: #{BotState.message_count}"
end
State Persistence
class StateManager
STATE_FILE = 'bot_state.json'
def self.load
if File.exist?(STATE_FILE)
JSON.parse(File.read(STATE_FILE))
else
{}
end
end
def self.save(state)
File.write(STATE_FILE, JSON.dump(state))
end
end
# Load state on startup
STATE = StateManager.load
# Save before reload
bot.hot_reload_manager.before_reload do
StateManager.save(STATE)
end
Reload Hooks
Before Reload
bot.hot_reload_manager.before_reload do
puts "Reloading..."
# Save state
# Cleanup resources
# Unregister temporary handlers
end
After Reload
bot.hot_reload_manager.after_reload do
puts "Reloaded!"
# Restore state
# Re-register handlers
# Verify bot health
end
On Error
bot.hot_reload_manager.on_error do |error|
puts "Reload failed: #{error}"
# Notify developer
# Log error
# Potentially revert
end
Limitations
What Can't Be Reloaded
- Bot configuration - Token, intents, etc.
- Active connections - WebSocket connections persist
- Running threads - Must manage thread lifecycle
- Global constants - Class/module definitions
Workarounds
# For constants, use methods instead
# ❌ Won't reload
VERSION = '1.0.0'
# ✅ Will reload
def version
'1.0.0' # Change this
end
# For class definitions, use composition
# ❌ Won't reload behavior
class MyHandler
def handle
# Old behavior
end
end
# ✅ Will reload
class MyHandler
def handle
# Delegate to reloadable module
reloadable_handle
end
def reloadable_handle
# Put actual logic here
end
end
Development Workflow
Typical Development Session
# 1. Start bot
ruby bot.rb
# => [Hot Reload] Watching lib/
# => [Hot Reload] Bot ready
# 2. Edit a file
vim lib/commands/ping.rb
# Change: "Pong!" to "Pong! v2"
# Save file
# 3. See reload
# => [Hot Reload] File changed: lib/commands/ping.rb
# => [Hot Reload] Reloading...
# => [Hot Reload] Reloaded successfully
# 4. Test in Discord
# Type /ping
# See: "Pong! v2"
Multiple Changes
bot.enable_hot_reload(
watch_dir: 'lib',
debounce: 2.0 # Wait 2 seconds after last change
)
With debounce, if you save 3 files quickly, it only reloads once after the last save.
Best Practices
1. Separate Code from Config
# ✅ Good - code in lib/
# lib/commands/ping.rb
bot.slash('ping', 'Ping!') { |cmd| ... }
# ❌ Bad - code mixed with config
# bot.rb
bot.slash('ping', 'Ping!') { |cmd| ... } # Can't reload easily
2. Use Idempotent Handlers
# ✅ Good - can be re-registered
bot.on(:message_create) do |event|
# Handle message
end
# ❌ Bad - accumulates on reload
counter = 0
bot.on(:message_create) do |event|
counter += 1 # This resets on reload!
end
3. Clean Up Resources
before_reload do
# Stop background threads
@worker_thread&.kill
# Close file handles
@log_file&.close
# Unsubscribe from external services
@webhook_client&.stop
end
after_reload do
# Restart resources
start_worker_thread
@log_file = File.open('new.log', 'a')
@webhook_client = WebhookClient.new
end
Debugging Reloads
Verbose Mode
bot.enable_hot_reload(
watch_dir: 'lib',
verbose: true # Log all reload activity
)
Output:
[Hot Reload] Watching: lib/
[Hot Reload] File changed: lib/commands/ping.rb
[Hot Reload] Calculating changes...
[Hot Reload] Reloading: lib/commands/ping.rb
[Hot Reload] Success: lib/commands/ping.rb
[Hot Reload] Total reload time: 45ms
Reload History
# Get recent reloads
history = bot.hot_reload_manager.history
history.each do |reload|
puts "#{reload.time}: #{reload.file}"
puts " Status: #{reload.status}"
puts " Error: #{reload.error}" if reload.error
end
Complete Example
require 'discord_rda'
# Load configuration
CONFIG = {
token: ENV['DISCORD_TOKEN'],
application_id: ENV['DISCORD_APP_ID'],
intents: [:guilds, :guild_messages]
}
bot = DiscordRDA::Bot.new(**CONFIG)
# State that persists across reloads
$message_count = 0
$user_commands = {}
# Load all command files
Dir['lib/commands/*.rb'].each { |f| load f }
# Load all event handlers
Dir['lib/events/*.rb'].each { |f| load f }
# Enable hot reload with hooks
bot.enable_hot_reload(
watch_dir: 'lib',
extensions: ['.rb'],
debounce: 1.0,
verbose: true
)
# Pre-reload hook
bot.hot_reload_manager.before_reload do
puts "[Reload] Saving state..."
# State is in global variables, automatically preserved
# (in production, use proper state management)
end
# Post-reload hook
bot.hot_reload_manager.after_reload do
puts "[Reload] Restoring state..."
puts "[Reload] Message count: #{$message_count}"
puts "[Reload] User commands: #{$user_commands.length}"
end
puts "Starting bot..."
puts "Hot reload enabled - edit files in lib/ to see changes"
bot.run
lib/commands/ping.rb
# This file can be edited and reloaded
bot.slash('ping', 'Check bot latency') do |cmd|
cmd.handler do |interaction|
$message_count += 1
interaction.respond(
content: "🏓 Pong! (processed #{$message_count} messages)"
)
end
end
lib/events/ready.rb
bot.on(:ready) do |event|
puts "Bot ready as #{event.user.username}"
puts "Hot reload active - make some changes!"
end
Troubleshooting
Reload Not Working
# Check watch directory exists
puts File.exist?('lib') # Should be true
# Check file extensions
bot.enable_hot_reload(
watch_dir: 'lib',
extensions: ['.rb'] # Must match your files
)
# Check with verbose mode
bot.enable_hot_reload(
watch_dir: 'lib',
verbose: true
)
# Look for "File changed" messages
Syntax Errors on Reload
# Reload hooks help identify issues
bot.hot_reload_manager.on_error do |error|
puts "❌ Reload failed!"
puts error.message
puts error.backtrace.first(5)
# Optionally notify via Discord
admin_user = bot.user(ADMIN_ID)
admin_user.dm("Reload failed: #{error.message}")
end
State Loss
# ❌ Local variable - lost on reload
counter = 0
# ✅ Global variable - preserved
counter = 0 # Define at top-level
# ✅ Constant with refresh
def config
@config ||= load_config # Lazy load, refreshable
end
Next Steps
- Plugin System - Modular code organization
- Examples - Complete working examples