diff --git a/spec/vendor/ai_inflector_spec.cr b/spec/vendor/ai_inflector_spec.cr index f22e724..a5dbe2c 100644 --- a/spec/vendor/ai_inflector_spec.cr +++ b/spec/vendor/ai_inflector_spec.cr @@ -27,6 +27,18 @@ describe AmberCLI::Vendor::Inflector::AITransformer do stats = AmberCLI::Vendor::Inflector::AITransformer.cache_stats stats[:size].should eq(0) end + + it "handles concurrent cache access safely" do + AmberCLI::Vendor::Inflector::AITransformer.clear_cache + channel = Channel(Nil).new + 10.times do + spawn do + AmberCLI::Vendor::Inflector::AITransformer.cache_stats + channel.send(nil) + end + end + 10.times { channel.receive } + end end describe "transform_with_ai" do diff --git a/src/amber_cli/commands/database.cr b/src/amber_cli/commands/database.cr index f41b19f..4355451 100644 --- a/src/amber_cli/commands/database.cr +++ b/src/amber_cli/commands/database.cr @@ -104,7 +104,7 @@ module AmberCLI::Commands when "create" Micrate::Cli.create_database(url) when "seed" - Amber::CLI::Helpers.run("crystal db/seeds.cr", wait: true, shell: true) + Amber::CLI::Helpers.run("crystal db/seeds.cr", wait: true) info "Seeded database" when "migrate" Micrate::Cli.run_up(url, MIGRATIONS_DIR) diff --git a/src/amber_cli/helpers/helpers.cr b/src/amber_cli/helpers/helpers.cr index 1816bbe..433d360 100644 --- a/src/amber_cli/helpers/helpers.cr +++ b/src/amber_cli/helpers/helpers.cr @@ -77,11 +77,21 @@ module Amber::CLI::Helpers File.write(app_file_path, application.gsub(injection_marker, replacement)) if deps.size > 0 end - def self.run(command, wait = true, shell = true) - if wait - Process.run(command, shell: shell, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + def self.run(command : String, wait = true, shell = false) + if shell + if wait + Process.run(command, shell: true, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + else + Process.new(command, shell: true, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + end else - Process.new(command, shell: shell, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + parsed_args = Process.parse_arguments(command) + cmd = parsed_args.shift? || return nil + if wait + Process.run(cmd, args: parsed_args, shell: false, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + else + Process.new(cmd, args: parsed_args, shell: false, output: Process::Redirect::Inherit, error: Process::Redirect::Inherit) + end end rescue ex : IO::Error # typically means we could not find the executable diff --git a/src/amber_cli/helpers/process_runner.cr b/src/amber_cli/helpers/process_runner.cr index 6a22768..15c95cb 100644 --- a/src/amber_cli/helpers/process_runner.cr +++ b/src/amber_cli/helpers/process_runner.cr @@ -171,7 +171,7 @@ module Sentry end private def start_process(run_command_run) - process = Amber::CLI::Helpers.run(run_command_run, wait: false, shell: false) + process = Amber::CLI::Helpers.run(run_command_run, wait: false) if process.is_a? Process @processes["run"] ||= Array(Process).new @processes["run"] << process @@ -189,11 +189,11 @@ module Sentry if (build_command = @build_commands[task]?) && !skip_build log task, "Building..." - build_result = Amber::CLI::Helpers.run(build_command) + build_result = Amber::CLI::Helpers.run(build_command, wait: true, shell: true) next unless build_result.is_a? Process::Status if build_result.success? - Amber::CLI::Helpers.run(build_command) + Amber::CLI::Helpers.run(build_command, wait: true, shell: true) else log task, "Build step failed." next # don't continue to run command step diff --git a/src/amber_cli/vendor/inflector/ai_transformer.cr b/src/amber_cli/vendor/inflector/ai_transformer.cr index c54555d..e84b750 100644 --- a/src/amber_cli/vendor/inflector/ai_transformer.cr +++ b/src/amber_cli/vendor/inflector/ai_transformer.cr @@ -23,7 +23,11 @@ module AmberCLI::Vendor::Inflector::AITransformer end end + # Cache limit to prevent memory leaks in persistent instances + MAX_CACHE_SIZE = 1000 + # Cache for AI transformation results + @@cache_mutex = Mutex.new @@cache = {} of String => String @@config = Config.new @@ -40,13 +44,23 @@ module AmberCLI::Vendor::Inflector::AITransformer # Check cache first cache_key = "#{word}:#{transformation}" - if cached_result = @@cache[cache_key]? - return cached_result + @@cache_mutex.synchronize do + if cached_result = @@cache[cache_key]? + # Move to end (most recently used) + @@cache.delete(cache_key) + @@cache[cache_key] = cached_result + return cached_result + end end # Try AI transformation if result = call_ai_service(word, transformation) - @@cache[cache_key] = result + @@cache_mutex.synchronize do + if @@cache.size >= MAX_CACHE_SIZE + @@cache.delete(@@cache.first_key) + end + @@cache[cache_key] = result + end return result end @@ -55,15 +69,19 @@ module AmberCLI::Vendor::Inflector::AITransformer # Clear the transformation cache def clear_cache - @@cache.clear + @@cache_mutex.synchronize do + @@cache.clear + end end # Get cache statistics def cache_stats - { - size: @@cache.size, - keys: @@cache.keys.sort, - } + @@cache_mutex.synchronize do + { + size: @@cache.size, + keys: @@cache.keys.sort, + } + end end # Call AI service to transform the word