Skip to content

Commit bf096b4

Browse files
Add GCS bucket HNS and colocation validation to orchestrator and skill (#165)
* Add GCS bucket HNS and colocation validation to orchestrator and skill
1 parent 123b7bc commit bf096b4

2 files changed

Lines changed: 109 additions & 3 deletions

File tree

npi/.gemini/skills/run-gcsfuse-npi/SKILL.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,34 @@ For each GCE VM target that requires preparation (e.g. if freshly created or res
182182

183183
Once the orchestrator outputs `SUCCESS`:
184184

185-
1. Execute the results query script:
185+
1. **Extract Custom Run Results**:
186+
To extract the performance results of a custom benchmark run, execute a `bq` query command.
187+
188+
> [!IMPORTANT]
189+
> **JSON Key Spacing**: In the FIO JSON output, the version is stored under the key `"fio version"` (with a space). Always query it using the quoted format: `JSON_VALUE(fio_json_output, '$."fio version"')` to avoid returning `NULL`.
190+
191+
Run the following query substituting your Cloud Project, Dataset ID, and Table Name (e.g. `fio_read_grpc` or `fio_write_grpc`):
192+
```bash
193+
bq query --project_id=<PROJECT_ID> --use_legacy_sql=false \
194+
"SELECT
195+
run_timestamp,
196+
iteration,
197+
JSON_VALUE(fio_json_output, '\$.\"fio version\"') AS fio_version,
198+
AVG(SAFE_CAST(JSON_VALUE(job.read.bw) AS FLOAT64)) / 1024.0 AS avg_read_bw_mib,
199+
AVG(SAFE_CAST(JSON_VALUE(job.write.bw) AS FLOAT64)) / 1024.0 AS avg_write_bw_mib
200+
FROM
201+
\`<PROJECT_ID>.<DATASET_ID>.<TABLE_ID>\`,
202+
UNNEST(JSON_EXTRACT_ARRAY(fio_json_output.jobs)) AS job
203+
GROUP BY 1, 2, 3
204+
ORDER BY run_timestamp DESC"
205+
```
206+
2. **Compare Against Baselines (Optional)**:
207+
If running comparison baseline tests, you can execute the results comparison script:
186208
```bash
187209
python3 query_results.py
188210
```
189-
2. Compile the throughput comparison report.
190-
3. **High-Performance Machine Type Verification**:
211+
3. Compile the throughput comparison report.
212+
4. **High-Performance Machine Type Verification**:
191213
* Check if the GCE VM or GKE node machine type used in the test (e.g., `c4-standard-96`, `ct6e-standard-4t`) is classified under the high-performance machine types in the GCSFuse `params.yaml` configuration file (located in the main GCSFuse repository).
192214
* If the machine type is missing, raise a Pull Request (PR) in the GCSFuse repository to add it. This ensures GCSFuse dynamically applies optimal configuration defaults (such as Direct Path, large connection pools, and high read-ahead) for this machine family in production.
193215

@@ -198,6 +220,8 @@ To ensure that the executing agent does not skip or miss any critical setup, run
198220
- [ ] **Prerequisites Verification**:
199221
- GCE boot disk size verified (>= 200GB).
200222
- GCE RAID0 SSD mount verified at `/mnt/lssd`.
223+
- Target VM and GCS bucket colocation verified (same zone for RAPID bucket, same region for regional bucket).
224+
- Target GCS bucket verified to have Hierarchical Namespace (HNS) enabled.
201225
- GKE TPU cluster node requirements verified (at least 1 CPU node + 1 TPU node active).
202226
- Remote python output buffering set to unbuffered (`python3 -u` verified).
203227
- [ ] **Startup Cleanup**:

npi/npi_orchestrator.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,81 @@ def execute_target(target, args, state_lock, state):
531531
else:
532532
print(f"[{target_name}] Run already completed successfully.")
533533

534+
def validate_colocation(target, project_id):
535+
"""Validates that GCS bucket has HNS enabled and is colocated with the VM."""
536+
bucket_name = target["bucket"]
537+
if bucket_name.startswith("gs://"):
538+
bucket_name = bucket_name[5:]
539+
540+
# For GKE, the benchmarks run on the GKE cluster, so we use its location.
541+
# For GCE, they run on the GCE VM itself.
542+
if target.get("type") == "gke":
543+
run_location = target.get("location") or target["zone"]
544+
else:
545+
run_location = target["zone"]
546+
run_location = run_location.lower()
547+
548+
is_rapid = target.get("is_rapid_bucket", False)
549+
550+
cmd = [
551+
"gcloud", "storage", "buckets", "describe",
552+
f"gs://{bucket_name}",
553+
f"--project={project_id}",
554+
"--raw",
555+
"--format=json"
556+
]
557+
try:
558+
res = subprocess.run(cmd, capture_output=True, text=True, check=True)
559+
meta = json.loads(res.stdout)
560+
if not isinstance(meta, dict):
561+
raise ValueError("Unexpected metadata format (expected a JSON object).")
562+
except subprocess.CalledProcessError as e:
563+
error_msg = e.stderr.strip() if e.stderr else str(e)
564+
raise ValueError(f"Failed to describe GCS bucket '{bucket_name}': {error_msg}")
565+
except Exception as e:
566+
raise ValueError(f"Failed to describe GCS bucket '{bucket_name}': {e}")
567+
568+
# Validate HNS
569+
hns_meta = meta.get("hierarchicalNamespace")
570+
hns_enabled = hns_meta.get("enabled", False) if isinstance(hns_meta, dict) else False
571+
if not hns_enabled:
572+
raise ValueError(f"Bucket '{bucket_name}' does not have Hierarchical Namespace (HNS) enabled. NPI benchmarks require HNS.")
573+
574+
raw_location = meta.get("location")
575+
location = raw_location.lower() if isinstance(raw_location, str) else ""
576+
raw_location_type = meta.get("locationType")
577+
location_type = raw_location_type.lower() if isinstance(raw_location_type, str) else ""
578+
579+
if is_rapid:
580+
if location_type != "zone":
581+
raise ValueError(f"Bucket '{bucket_name}' is configured as a RAPID bucket, but GCS location type is '{location_type}' (expected 'zone').")
582+
583+
raw_data_locs = meta.get("dataLocations")
584+
data_locs = [loc.lower() for loc in raw_data_locs if isinstance(loc, str)] if isinstance(raw_data_locs, list) else []
585+
if not data_locs:
586+
raise ValueError(f"Bucket '{bucket_name}' has no data locations listed in GCS metadata.")
587+
588+
# If run_location is a zone (e.g. us-central1-a), ensure it is in data_locs.
589+
# If run_location is a region (e.g. us-central1), ensure at least one data_loc is in that region.
590+
loc_parts = run_location.split("-")
591+
is_zone = loc_parts[-1].isalpha() and len(loc_parts[-1]) == 1
592+
if is_zone:
593+
if run_location not in data_locs:
594+
raise ValueError(f"Colocation Error: RAPID bucket '{bucket_name}' is in zone(s) {data_locs}, but target is in zone '{run_location}'. They must be in the same zone.")
595+
else:
596+
region_prefix = run_location + "-"
597+
if not any(loc.startswith(region_prefix) for loc in data_locs):
598+
raise ValueError(f"Colocation Error: RAPID bucket '{bucket_name}' is in zone(s) {data_locs}, but target is in region '{run_location}'. The bucket zone must be within the target region.")
599+
else:
600+
loc_parts = run_location.split("-")
601+
is_zone = loc_parts[-1].isalpha() and len(loc_parts[-1]) == 1
602+
run_region = "-".join(loc_parts[:-1]) if is_zone else run_location
603+
if location_type != "region":
604+
raise ValueError(f"Bucket '{bucket_name}' is configured as a regional bucket, but GCS location type is '{location_type}' (expected 'region').")
605+
606+
if location != run_region:
607+
raise ValueError(f"Colocation Error: Regional bucket '{bucket_name}' is in region '{location}', but target is in region '{run_region}'. They must be in the same region.")
608+
534609
def main():
535610
parser = argparse.ArgumentParser(description="GCSFuse NPI Orchestrator")
536611
parser.add_argument("--config", default="targets.json", help="Path to targets.json configuration file")
@@ -579,6 +654,13 @@ def main():
579654
print(f"Error parsing configuration file {config_path}: {e}", file=sys.stderr)
580655
sys.exit(1)
581656

657+
for t in targets:
658+
try:
659+
validate_colocation(t, PROJECT_ID)
660+
except Exception as e:
661+
print(f"Validation failed for target '{t.get('name', 'unknown')}': {e}", file=sys.stderr)
662+
sys.exit(1)
663+
582664
state = load_state(targets)
583665
print(f"Current State: {json.dumps(state, indent=2)}")
584666

0 commit comments

Comments
 (0)