diff --git a/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb b/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb index 7c0a9ff..da42d5a 100644 --- a/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb +++ b/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb @@ -84,7 +84,8 @@ def depsolve(request, unsolved, desired_versions, environment_constraints, cookb new_unsolved = unsolved[1..-1] # Pick this cookbook, and add dependencies - cookbook_obj = FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..1] + ["cookbooks", solve_for, desired_version])) + cookbook_obj = cookbook_obj_cache.dig(solve_for, desired_version) || + FFI_Yajl::Parser.parse(get_data(request, request.rest_path[0..1] + ["cookbooks", solve_for, desired_version])) cookbook_obj_cache[solve_for] ||= {} cookbook_obj_cache[solve_for][desired_version] = cookbook_obj cookbook_metadata = cookbook_obj["metadata"] || {} @@ -110,7 +111,8 @@ def depsolve(request, unsolved, desired_versions, environment_constraints, cookb next if dep_not_found # Depsolve children with this desired version! First solution wins. - result, cookbook_obj_cache = depsolve(request, new_unsolved, new_desired_versions, environment_constraints, cookbook_obj_cache) + result, returned_cache = depsolve(request, new_unsolved, new_desired_versions, environment_constraints, cookbook_obj_cache) + cookbook_obj_cache = returned_cache || cookbook_obj_cache return [result, cookbook_obj_cache] if result end [nil, nil] diff --git a/spec/endpoints/environment_cookbook_versions_endpoint_spec.rb b/spec/endpoints/environment_cookbook_versions_endpoint_spec.rb index 9e255a3..a488aec 100644 --- a/spec/endpoints/environment_cookbook_versions_endpoint_spec.rb +++ b/spec/endpoints/environment_cookbook_versions_endpoint_spec.rb @@ -362,6 +362,42 @@ def cookbook_json(name, version, dependencies = {}) expect(result).to be_nil end + it "uses cached cookbook data instead of re-fetching during backtracking" do + desired = { + "web" => ["1.0.0", "2.0.0"], + "db" => ["1.0.0"], + } + # web 2.0.0 depends on lib, which depends on missing (doesn't exist) → branch fails + # web 1.0.0 has no deps → backtracks here, db must be re-solved + # db 1.0.0 should be fetched only once (cached from the first branch) + allow(data_store).to receive(:get) + .with(org_prefix + ["cookbooks", "web", "2.0.0"], request) + .and_return(cookbook_json("web", "2.0.0", { "lib" => ">= 1.0.0" })) + allow(data_store).to receive(:get) + .with(org_prefix + ["cookbooks", "web", "1.0.0"], request) + .and_return(cookbook_json("web", "1.0.0")) + expect(data_store).to receive(:get) + .with(org_prefix + ["cookbooks", "db", "1.0.0"], request) + .once + .and_return(cookbook_json("db", "1.0.0")) + allow(data_store).to receive(:exists_dir?) + .with(org_prefix + %w{cookbooks lib}) + .and_return(true) + allow(data_store).to receive(:list) + .with(org_prefix + %w{cookbooks lib}) + .and_return(["1.0.0"]) + allow(data_store).to receive(:get) + .with(org_prefix + ["cookbooks", "lib", "1.0.0"], request) + .and_return(cookbook_json("lib", "1.0.0", { "missing" => ">= 1.0.0" })) + allow(data_store).to receive(:exists_dir?) + .with(org_prefix + %w{cookbooks missing}) + .and_return(false) + + result, _cache = endpoint.depsolve(request, %w{web db}, desired, {}) + expect(result["web"]).to eq(["1.0.0"]) + expect(result["db"]).to eq(["1.0.0"]) + end + it "resolves a deep dependency chain" do desired = { "app" => ["1.0.0"] } allow(data_store).to receive(:get)