Fix PathNotFound error in msexports ETL ingestion on first run - #2247
Fix PathNotFound error in msexports ETL ingestion on first run#2247Michael Flanakin (flanakin) wants to merge 1 commit into
Conversation
The "Get Existing Parquet Files" GetMetadata activity in both msexports_ETL_ingestion (Microsoft.CostManagement/Exports/app.bicep) and the analytics ingestion ETL pipeline (Microsoft.FinOpsHubs/Analytics/app.bicep) failed with PathNotFound when the destination folder had never been created, e.g. the first ingestion for a scope/dataset/month combination. Recommendation exports hit this most often because their path includes an extra exportName segment, but any export type can hit it on first run. Add 'exists' to the GetMetadata fieldList, which is ADF's documented way to make the activity return exists:false instead of failing when the path is missing. This mirrors the pattern already used by other GetMetadata activities in this codebase (Check Schema in Exports/app.bicep, and the two GetMetadata activities in IngestionQueries/app.bicep). Update the downstream Filter activity's items expression to check .output.exists before reading childItems, so a missing folder now resolves to an empty file list instead of failing the pipeline. Fixes #2088
| typeProperties: { | ||
| items: { | ||
| value: '@if(contains(activity(\'Get Existing Parquet Files\').output, \'childItems\'), activity(\'Get Existing Parquet Files\').output.childItems, json(\'[]\'))' | ||
| value: '@if(and(activity(\'Get Existing Parquet Files\').output.exists, contains(activity(\'Get Existing Parquet Files\').output, \'childItems\')), activity(\'Get Existing Parquet Files\').output.childItems, json(\'[]\'))' |
There was a problem hiding this comment.
Unguarded .output.exists breaks the Completed dependency tolerance.
Filter Out Current Exports depends on Get Existing Parquet Files with dependencyConditions: ['Completed'] (not Succeeded) - it is deliberately designed to run even when the GetMetadata activity fails. That is exactly why the original expression used only contains(output, 'childItems'): a failed activity returns an output object without that property, so the expression degraded to json('[]') and the pipeline continued. (Both the Completed condition and the contains guard were introduced together in #1512.)
Adding exists to the fieldList stops the PathNotFound failure, but the activity can still fail for other reasons - transient ADLS Gen2 5xx/throttling, an RBAC error on the ingestion container, or the 12h timeout. In those cases output has no exists property either, so and(activity('Get Existing Parquet Files').output.exists, ...) now fails evaluation ("the expression cannot be evaluated because property 'exists' doesn't exist"), the Filter activity fails, For Each Old File (Succeeded dependency) is skipped, and msexports_ETL_ingestion fails - where it previously degraded gracefully to an empty file list.
The output.exists term is also redundant: when exists is false, GetMetadata returns no childItems, so the pre-existing contains(output, 'childItems') check alone already yields json('[]'). The simplest fix is to keep only the fieldList addition and leave this expression unchanged (or, if you want the explicit check, guard it with contains(activity('Get Existing Parquet Files').output, 'exists') first).
Note the precedent cited in the PR description (IngestionQueries/app.bicep:363) is safe only because that Filter uses a Succeeded dependency, so it never evaluates against a failed-activity output.
| value: '@if(and(activity(\'Get Existing Parquet Files\').output.exists, contains(activity(\'Get Existing Parquet Files\').output, \'childItems\')), activity(\'Get Existing Parquet Files\').output.childItems, json(\'[]\'))' | |
| value: '@if(contains(activity(\'Get Existing Parquet Files\').output, \'childItems\'), activity(\'Get Existing Parquet Files\').output.childItems, json(\'[]\'))' |
| } | ||
| } | ||
| fieldList: [ | ||
| 'exists' |
There was a problem hiding this comment.
This changes which error the user gets, but the pipeline still fails on a missing folder.
When ingestion_ExecuteETL runs for a folderPath whose ingestion folder does not exist yet, Get Existing Parquet Files now succeeds with exists: false, Filter Out Folders yields an empty array, and then If No Files (below, @equals(length(activity('Filter Out Folders').output.Value), 0)) fires the Files Not Found Fail activity with errorCode IngestionFilesNotFound.
So the cascade into ingestion_ExecuteETL described in #2088 is not actually resolved here - PathNotFound is just replaced by IngestionFilesNotFound, whose message ("Please confirm the folder path is the full path, including the "ingestion" container and not starting with or ending with a slash") points the operator at a path-formatting problem that is not the real cause.
If a first-run / no-data folder should be a no-op, If No Files needs to distinguish "folder absent" (exists == false) from "folder present but empty / path malformed". If failing loudly is still the intent here, it would be worth saying so in the PR description, since the current text implies this hunk fixes the failure.
Summary
GetMetadataactivity fails withPathNotFoundwhen the destination folder doesn't exist yet (e.g. first ingestion for a scope/dataset/month combination). Reservation recommendation exports hit this most often because their destination path adds an extraexportNamesegment, but any export type can hit it on a first run.msexports_ETL_ingestion(Microsoft.CostManagement/Exports/app.bicep) and the analytics ingestion ETL pipeline (Microsoft.FinOpsHubs/Analytics/app.bicep).'exists'to theGetMetadataactivity'sfieldList. Per ADF's GetMetadata docs, specifyingexistsmakes the activity returnexists: falseinstead of throwing when the path is missing — "Ifexistsisn't specified in the field list, the Get Metadata activity fails if the object isn't found." The downstreamFilteractivity'sitemsexpression now checks.output.existsbefore reading.output.childItems, so a missing folder resolves to an empty list instead of propagating the failure.Check SchemainExports/app.bicep, and bothGetMetadataactivities inIngestionQueries/app.bicep(one of which — "Get Existing Parquet Files" — already combinesexists+childItemsexactly like this fix does).Root cause verification (re: issue #2088 comment)
A prior comment on the issue did solid root-cause analysis but its line numbers had drifted and its proposed fix options were speculative (custom error handling / parent-folder existence checks / wrapping in an If Condition). I re-verified against the current code:
Microsoft.CostManagement/Exports/app.bicep:GetMetadataactivity is at lines 1090-1122,Filter Out Current Exportsat 1123-1147 (comment's estimate of ~1090-1122 / ~1123-1147 was accurate).Microsoft.FinOpsHubs/Analytics/app.bicep: identical vulnerable pattern confirmed at lines 1668-1707 (comment said ~1667-1706, essentially correct).existsinfieldList), not a custom If Condition or Web-activity workaround — and this idiom was already present elsewhere in the repo, so this change makes the two vulnerable call sites consistent with the rest of the codebase rather than introducing a new pattern.Test plan
bicep build src/templates/finops-hub/modules/Microsoft.CostManagement/Exports/app.bicep --stdout— builds cleanlybicep build src/templates/finops-hub/modules/Microsoft.FinOpsHubs/Analytics/app.bicep --stdout— builds cleanly"exists"in bothfieldListarrayspwsh -Command "./src/scripts/Test-PowerShell.ps1 -Lint"— 3418/3418 passedFixes #2088