Skip to content

refactor(cache): lazy-load built-in definitions from cache on demand, add ExportBuiltInCache, and extend alzlibtool CLI#241

Closed
Copilot wants to merge 6 commits intomainfrom
copilot/237-improve-cache-integration
Closed

refactor(cache): lazy-load built-in definitions from cache on demand, add ExportBuiltInCache, and extend alzlibtool CLI#241
Copilot wants to merge 6 commits intomainfrom
copilot/237-improve-cache-integration

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 11, 2026

  • Add cache.NewCacheFromDefinitions constructor
  • Add AlzLib.ExportBuiltInCache() method
  • Add unit tests for ExportBuiltInCache
  • Extend alzlibtool cache create with library/architecture flags and --from-cache
  • Add --from-cache flag to alzlibtool generate architecture
  • Fix: deep-copy values in ExportBuiltInCache
  • Fix: reuse Azure clients per ensureReferencedPolicyDefinitions invocation
  • Fix: remove defer az.cache = nil from GetDefinitionsFromAzure (cache retained for AlzLib lifetime)
  • Fix: protect az.cache reads with az.mu.RLock() in policyDefinitionFromCache/policySetDefinitionFromCache
  • Fix: snapshot az.clients.policyClient under az.mu.RLock() in getBuiltInPolicies, getBuiltInPolicySets, and ensureReferencedPolicyDefinition
  • Fix: update ExportBuiltInCache docstring to include nil PolicyType in the inclusion criteria
Original prompt

The recent #237 pull request introduced a cache feature for the Azure/alzlib repository. However, the implementation overly simplified the feature, resulting in all Azure built-in definitions being cached and loaded into the alzlib struct. To optimize this, improvements are needed to wire the cache into the GetBuiltInDefinititions function, ensuring better integration. Additionally, the cache usage should be adjusted to allow the garbage collector (GC) to efficiently discard the cache afterwards.

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: The recent #237 PR introduced a cache feature. I think the implementation was too simple in that it resulted in ALL azure built in definitions being cached (loaded into the alzlib struct).

Make improvements by wiring the cache into the GetBuiltInDefinititions and allowing the GC to discard the cache afterwards.

Assistant: A pull request will be created to refine the cache integration for optimizing the GetBuiltInDefinititions functionality. I'll notify you once this is completed.

This pull request was created from Copilot chat.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

…ding and GC

- cache/cache.go: Add PolicyDefinitionVersionsByName and PolicySetDefinitionVersionsByName
  per-name lookup methods, avoiding full-map clone for individual lookups
- alzlib.go: Extend BuiltInCache interface with new per-name lookup methods
- alzlib.go: Add `cache BuiltInCache` field to AlzLib struct
- alzlib.go: AddCache now stores only the cache reference (lazy, not eager)
- alzlib.go: GetDefinitionsFromAzure defers az.cache = nil so GC can reclaim cache
- alzlib.go: getBuiltInPolicies checks cache before Azure; lazy-init Azure client
- alzlib.go: getBuiltInPolicySets checks cache before Azure; lazy-init Azure clients
- alzlib.go: ensureReferencedPolicyDefinitions removes upfront client creation
- alzlib.go: ensureReferencedPolicyDefinition checks AlzLib → cache → Azure in order
- alzlib.go: Extract policyDefinitionFromCache/policySetDefinitionFromCache helpers
- alzlib_test.go: Update mockBuiltInCache with new interface methods
- alzlib_test.go: Replace eager-load tests with lazy-load tests
- integrationtest/cache_test.go: Update TestCacheInitWithLibrary for lazy semantics
- docs/cache.md: Document lazy loading and GC behaviour

Co-authored-by: matt-FFFFFF <16320656+matt-FFFFFF@users.noreply.github.com>
Copilot AI changed the title [WIP] [237] Improve cache integration for built-in definitions refactor(cache): lazy-load built-in definitions from cache on demand Mar 11, 2026
@matt-FFFFFF
Copy link
Copy Markdown
Member

@copilot add a way to create a new cache once all defs have been loaded. A method on alzlib perhaps?

Should be possible to filter out any custom policies by their type or resource id.

I want to give callers the ability to generate a cache based on their specific use case - rather than using the alzlibtool cli to cache EVERYTHING.

…s constructor

- cache/cache.go: Add NewCacheFromDefinitions constructor that builds a Cache from
  existing PolicyDefinitionVersions/PolicySetDefinitionVersions maps
- alzlib.go: Add ExportBuiltInCache() method that collects only built-in and static
  definitions from the loaded AlzLib state, filtering out custom definitions
- alzlib.go: Add isBuiltInPolicyDefinitionVersions / isBuiltInPolicySetDefinitionVersions
  helpers for PolicyType-based filtering
- alzlib_test.go: Add 5 tests for ExportBuiltInCache covering the filter logic,
  save/reload round-trip, empty state, nil PolicyType, and static type

Co-authored-by: matt-FFFFFF <16320656+matt-FFFFFF@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 11, 2026

@copilot add a way to create a new cache once all defs have been loaded. A method on alzlib perhaps?

Should be possible to filter out any custom policies by their type or resource id....

Done in 73bbd44. Added two new APIs:

cache.NewCacheFromDefinitions(pds, psds) — constructs a *Cache directly from existing PolicyDefinitionVersions/PolicySetDefinitionVersions maps.

AlzLib.ExportBuiltInCache() *cache.Cache — snapshots only the built-in and static definitions currently loaded in AlzLib (filtering out PolicyTypeCustom). The typical workflow for a targeted cache is:

az := alzlib.NewAlzLib(nil)
az.AddPolicyClient(cf)

// Init loads library, GetDefinitionsFromAzure fetches only referenced built-ins
az.Init(ctx, lib)
deployment.NewHierarchy(az).FromArchitecture(ctx, "alz", subID, location)

// Export only the built-ins that were actually needed
c := az.ExportBuiltInCache()

var buf bytes.Buffer
c.Save(&buf)
// persist buf to a file for reuse

Definitions with PolicyTypeCustom are excluded. Definitions with PolicyTypeBuiltIn, PolicyTypeStatic, or nil PolicyType are included.

Copilot AI changed the title refactor(cache): lazy-load built-in definitions from cache on demand refactor(cache): lazy-load built-in definitions from cache on demand and add ExportBuiltInCache Mar 11, 2026
@matt-FFFFFF
Copy link
Copy Markdown
Member

@copilot ok now in the same fashion as alzlibtool generate architecture, extend the alzlibtool cache create to allow the specification of a library path and architecture.

Also add --use-cache (or better name) flags to the generate command and to cache create commands.

The will allow the efficient update of a cache. It should allow (but not mandate) the same source and destination file.

…ary+architecture mode to cache create

- generate/architecture.go: add --from-cache flag; loads seed cache into AlzLib
  before Init/FromArchitecture to reduce Azure API calls
- cache/create.go: add --library, --architecture, --from-cache, --rootmg, --location
  flags; new architecture-scoped mode uses alzlib.ExportBuiltInCache() to produce
  a minimal cache; same path allowed for --from-cache and --output (in-place update)

Co-authored-by: matt-FFFFFF <16320656+matt-FFFFFF@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 11, 2026

@copilot ok now in the same fashion as alzlibtool generate architecture, extend the alzlibtool cache create to allow the specification of a library path and architecture.

Also add --use-cache (or bet...

Done in a4bd09b. Changes:

alzlibtool cache create — new flags:

  • --library <path> + --architecture <name>: switches to architecture-scoped mode; processes the library and calls az.ExportBuiltInCache() to produce a minimal cache containing only the definitions actually referenced
  • --from-cache <file>: seeds AlzLib with an existing cache before fetching from Azure; the same path may be used for --from-cache and --output for in-place updates
  • --rootmg / --location: passed to FromArchitecture (same defaults as the generate command)

alzlibtool generate architecture — new flag:

  • --from-cache <file>: loads a cache into AlzLib before Init/FromArchitecture, reducing Azure API calls to only definitions absent from the cache

Example workflows:

# Generate a minimal architecture-scoped cache
alzlibtool cache create --library ./my-lib --architecture alz --output alz-cache.json.gz

# Update a cache in-place with definitions from a new/updated architecture  
alzlibtool cache create --library ./my-lib --architecture alz \
  --from-cache alz-cache.json.gz --output alz-cache.json.gz

# Generate architecture deployment seeded from cache (fewer Azure API calls)
alzlibtool generate architecture ./my-lib alz --from-cache alz-cache.json.gz

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • azcliprod.blob.core.windows.net
    • Triggering command: /usr/bin/../../opt/az/bin/python3 /usr/bin/../../opt/az/bin/python3 -Im azure.cli account get-access-token -o json --resource REDACTED ynctest.go l/linux_amd64/as-bool ux-amd64/pkg/too-buildtags ap sdk-for-go/sdk/a-errorsas om/!azure/azure--ifaceassert ux-amd64/pkg/tool/linux_amd64/vet s140/nistec/p256git pkg/mod/golang.oclone l/linux_amd64/coREDACTED ux-amd64/pkg/too/tmp/getter2757282598/temp (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title refactor(cache): lazy-load built-in definitions from cache on demand and add ExportBuiltInCache refactor(cache): lazy-load built-in definitions from cache on demand, add ExportBuiltInCache, and extend alzlibtool CLI Mar 11, 2026
@matt-FFFFFF matt-FFFFFF requested a review from Copilot March 11, 2026 23:14
@matt-FFFFFF matt-FFFFFF marked this pull request as ready for review March 11, 2026 23:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the built-in definitions cache integration to lazy-load built-ins on demand (cache → Azure fallback), adds an ExportBuiltInCache API for exporting a minimal cache from the currently-loaded built-ins, and extends alzlibtool to support cache seeding and architecture-scoped cache creation.

Changes:

  • Change AlzLib.AddCache from eager population to lazy cache lookup during GetDefinitionsFromAzure.
  • Add AlzLib.ExportBuiltInCache() and cache.NewCacheFromDefinitions(...) to export a minimal built-ins cache.
  • Extend alzlibtool cache create / alzlibtool generate architecture with --from-cache, and add architecture-scoped cache creation flags.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
integrationtest/cache_test.go Updates integration expectations for lazy cache loading (built-ins appear after architecture processing).
docs/cache.md Updates documentation to describe lazy cache lookup and lifecycle behavior.
cmd/alzlibtool/command/generate/architecture.go Adds --from-cache support to seed built-ins before running architecture generation.
cmd/alzlibtool/command/cache/create.go Adds architecture-scoped cache creation mode and --from-cache seeding for incremental updates.
cache/cache.go Adds constructor to build a cache from already-loaded definition/version maps and O(1) per-name accessors.
alzlib_test.go Updates mocks/tests for lazy cache semantics and adds ExportBuiltInCache coverage.
alzlib.go Implements lazy cache lookup, ExportBuiltInCache, and restructures referenced-definition fetching to avoid eager Azure client construction.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread alzlib.go
Comment thread alzlib.go
Comment thread alzlib.go
Comment thread alzlib.go Outdated
Comment thread alzlib.go
@matt-FFFFFF
Copy link
Copy Markdown
Member

@copilot address the UNRESOLVED review comments. All are valid I think. There is a resolved comment that's not applicable.

- ExportBuiltInCache: deep-copy *PolicyDefinitionVersions values so
  AlzLib mutations (e.g. SetAssignPermissionsOnDefinitionParameter)
  cannot affect the exported cache
- GetDefinitionsFromAzure: remove defer az.cache=nil; cache is now kept
  for AlzLib lifetime (callers release with AddCache(nil)). Fixes bug
  where only the first management group could use the cache.
- policyDefinitionFromCache / policySetDefinitionFromCache: snapshot
  az.cache under az.mu.RLock to prevent data race with AddCache
- ensureReferencedPolicyDefinitions: lazily create DefinitionsClient /
  DefinitionVersionsClient once per invocation and reuse across all
  policy-set sub-references instead of creating per cache miss
- Add TestExportBuiltInCacheIsDeepCopied and helper
  testBuiltInPolicyDefinitionWithParam
- Update AddCache docstring and docs/cache.md for new cache lifecycle

Co-authored-by: matt-FFFFFF <16320656+matt-FFFFFF@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 12, 2026

@copilot address the UNRESOLVED review comments. All are valid I think. There is a resolved comment that's not applicable.

Done in 3cb6a94. All three unresolved comments addressed:

  1. ExportBuiltInCache shallow copy — now deep-copies each *PolicyDefinitionVersions / *PolicySetDefinitionVersions so that subsequent SetAssignPermissionsOnDefinitionParameter calls in AlzLib cannot affect the exported cache. Added TestExportBuiltInCacheIsDeepCopied to cover this.

  2. Client creation per miss in ensureReferencedPolicyDefinitionDefinitionsClient and DefinitionVersionsClient are now lazily created once per ensureReferencedPolicyDefinitions invocation and reused across all sub-references, rather than being re-created on every Azure-needed cache miss.

  3. Cache clearing after GetDefinitionsFromAzure + data race — removed defer az.cache = nil (which was also causing a bug where only the first management group in a multi-MG architecture could use the cache). The cache is now kept for the lifetime of AlzLib; callers release it with AddCache(nil). Both policyDefinitionFromCache and policySetDefinitionFromCache now snapshot az.cache under az.mu.RLock() to eliminate the data race with concurrent AddCache calls.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.


You can also share your feedback on Copilot code review. Take the survey.

Comment thread alzlib.go Outdated
Comment on lines +573 to +576
// set definitions that are currently loaded in AlzLib. Only definitions with a
// [armpolicy.PolicyTypeBuiltIn] or [armpolicy.PolicyTypeStatic] policy type are included;
// custom definitions (loaded from library files) are excluded.
//
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExportBuiltInCache docstring says only PolicyTypeBuiltIn/PolicyTypeStatic are included, but isBuiltInPolicyDefinitionVersions/isBuiltInPolicySetDefinitionVersions also treat a nil PolicyType as built-in and therefore include those definitions. Update the docstring (and/or the referenced comment) so it matches the actual inclusion rules.

Suggested change
// set definitions that are currently loaded in AlzLib. Only definitions with a
// [armpolicy.PolicyTypeBuiltIn] or [armpolicy.PolicyTypeStatic] policy type are included;
// custom definitions (loaded from library files) are excluded.
//
// set definitions that are currently loaded in AlzLib. Definitions are included when their
// policy type is [armpolicy.PolicyTypeBuiltIn], [armpolicy.PolicyTypeStatic], or is
// unspecified/nil (which is treated as built-in); custom definitions (loaded from library
// files) are excluded.

Copilot uses AI. Check for mistakes.
Comment thread alzlib.go Outdated
Comment on lines +940 to +947
if az.clients.policyClient == nil {
return errors.New("Alzlib.getBuiltInPolicies: policy client not set")
}

// Lazily initialise clients on first Azure call.
if client == nil {
versionedClient = az.clients.policyClient.NewDefinitionVersionsClient()
client = az.clients.policyClient.NewDefinitionsClient()
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getBuiltInPolicies reads az.clients.policyClient without holding az.mu, while AddPolicyClient writes it under az.mu.Lock(). This can still produce a data race if GetDefinitionsFromAzure runs concurrently with AddPolicyClient. Consider snapshotting the policy client under az.mu.RLock() (similar to the cache snapshot) and using the snapshot for the remainder of the call.

Suggested change
if az.clients.policyClient == nil {
return errors.New("Alzlib.getBuiltInPolicies: policy client not set")
}
// Lazily initialise clients on first Azure call.
if client == nil {
versionedClient = az.clients.policyClient.NewDefinitionVersionsClient()
client = az.clients.policyClient.NewDefinitionsClient()
az.mu.RLock()
policyClient := az.clients.policyClient
az.mu.RUnlock()
if policyClient == nil {
return errors.New("Alzlib.getBuiltInPolicies: policy client not set")
}
// Lazily initialise clients on first Azure call.
if client == nil {
versionedClient = policyClient.NewDefinitionVersionsClient()
client = policyClient.NewDefinitionsClient()

Copilot uses AI. Check for mistakes.
Comment thread alzlib.go Outdated
Comment on lines +998 to +1005
if az.clients.policyClient == nil {
return errors.New("Alzlib.getBuiltInPolicySets: policy client not set")
}

// Lazily initialise clients on first Azure call.
if client == nil {
versionedClient = az.clients.policyClient.NewSetDefinitionVersionsClient()
client = az.clients.policyClient.NewSetDefinitionsClient()
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getBuiltInPolicySets reads az.clients.policyClient without holding az.mu, while AddPolicyClient writes it under az.mu.Lock(). This can lead to a data race if GetDefinitionsFromAzure (or callers) run concurrently with AddPolicyClient. Consider snapshotting the policy client under az.mu.RLock() and using that local reference for Azure fallbacks.

Suggested change
if az.clients.policyClient == nil {
return errors.New("Alzlib.getBuiltInPolicySets: policy client not set")
}
// Lazily initialise clients on first Azure call.
if client == nil {
versionedClient = az.clients.policyClient.NewSetDefinitionVersionsClient()
client = az.clients.policyClient.NewSetDefinitionsClient()
az.mu.RLock()
policyClient := az.clients.policyClient
az.mu.RUnlock()
if policyClient == nil {
return errors.New("Alzlib.getBuiltInPolicySets: policy client not set")
}
// Lazily initialise clients on first Azure call.
if client == nil {
versionedClient = policyClient.NewSetDefinitionVersionsClient()
client = policyClient.NewSetDefinitionsClient()

Copilot uses AI. Check for mistakes.
Comment thread alzlib.go Outdated
Comment on lines +1310 to +1324
// Fall back to Azure.
if az.clients.policyClient == nil {
return errors.New("Alzlib.ensureReferencedPolicyDefinition: policy client not set")
}

if ref.DefinitionVersion != nil {
return az.fetchReferencedPolicyDefinitionVersions(ctx, definitionsVersionedClient, resID.Name, setDefinition, ref)
if *definitionsVersionedClient == nil {
*definitionsVersionedClient = az.clients.policyClient.NewDefinitionVersionsClient()
}

return az.fetchReferencedPolicyDefinitionVersions(ctx, *definitionsVersionedClient, resID.Name, setDefinition, ref)
}

if *definitionsClient == nil {
*definitionsClient = az.clients.policyClient.NewDefinitionsClient()
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensureReferencedPolicyDefinition reads az.clients.policyClient without synchronization, but AddPolicyClient writes it under az.mu.Lock(). If callers can invoke these concurrently, this is a data race. Snapshot policyClient under az.mu.RLock() (or otherwise synchronize access) before using it to lazily create the Azure clients.

Copilot uses AI. Check for mistakes.
@matt-FFFFFF
Copy link
Copy Markdown
Member

@copilot four more review comments. All seem valid. Please take a look.

Ignore the resolved comment threads. These have already been implemented or addressed.

…xportBuiltInCache docstring

- getBuiltInPolicies: snapshot az.clients.policyClient under az.mu.RLock()
  before the Azure fallback to prevent data race with AddPolicyClient
- getBuiltInPolicySets: same fix
- ensureReferencedPolicyDefinition: same fix
- ExportBuiltInCache: update docstring to accurately reflect that nil
  PolicyType is also included (treated as built-in), not only
  PolicyTypeBuiltIn and PolicyTypeStatic

Co-authored-by: matt-FFFFFF <16320656+matt-FFFFFF@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 12, 2026

@copilot four more review comments. All seem valid. Please take a look.

Ignore the resolved comment threads. These have already been implemented or addressed.

Done in 28aa64c:

  1. Docstring — updated ExportBuiltInCache to state that nil PolicyType is also included (treated as built-in), not only PolicyTypeBuiltIn/PolicyTypeStatic.

  2. getBuiltInPolicies data raceaz.clients.policyClient is now snapshotted under az.mu.RLock() before use.

  3. getBuiltInPolicySets data race — same fix applied.

  4. ensureReferencedPolicyDefinition data raceaz.clients.policyClient is now snapshotted under az.mu.RLock() before the lazy client initialisation.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.40580% with 170 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.71%. Comparing base (b7241fb) to head (28aa64c).

Files with missing lines Patch % Lines
cmd/alzlibtool/command/cache/create.go 0.00% 120 Missing ⚠️
alzlib.go 78.40% 22 Missing and 5 partials ⚠️
cache/cache.go 0.00% 12 Missing ⚠️
cmd/alzlibtool/command/generate/architecture.go 42.10% 10 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #241      +/-   ##
==========================================
- Coverage   48.43%   47.71%   -0.72%     
==========================================
  Files          54       54              
  Lines        4590     4824     +234     
==========================================
+ Hits         2223     2302      +79     
- Misses       2087     2238     +151     
- Partials      280      284       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@matt-FFFFFF
Copy link
Copy Markdown
Member

Thank you for your contribution @app/copilot-swe-agent!

Due to CI/CD limitations with fork PRs and secrets access, I've migrated your changes to #242. All your commits and authorship have been preserved. Please follow the new PR for any updates or requested changes.

The new PR will run all required checks and can be merged once approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants