Skip to content

Commit c745035

Browse files
committed
Enhance map analysis by enforcing two standards input and improving Redis fallback handling
1 parent a54bf40 commit c745035

1 file changed

Lines changed: 67 additions & 49 deletions

File tree

application/web/web_main.py

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,6 @@
4848

4949

5050
ITEMS_PER_PAGE = 20
51-
OWASP_TOP10_2025_DATA_FILE = (
52-
pathlib.Path(__file__).resolve().parent.parent
53-
/ "utils"
54-
/ "external_project_parsers"
55-
/ "data"
56-
/ "owasp_top10_2025.json"
57-
)
5851
OPENCRE_STANDARD_NAME = "OpenCRE"
5952

6053
app = Blueprint(
@@ -422,8 +415,12 @@ def map_analysis() -> Any:
422415
posthog.capture(f"map_analysis", f"standards:{standards}")
423416

424417
database = db.Node_collection()
418+
if len(standards) < 2:
419+
abort(400, "Please provide two standards")
420+
standards = standards[:2]
425421
standards_hash = gap_analysis.make_resources_key(standards)
426422

423+
# ----- PR #825: OpenCRE fast path -----
427424
if OPENCRE_STANDARD_NAME in standards:
428425
direct_gap_analysis = _build_direct_cre_overlap_map_analysis(
429426
standards, standards_hash, database
@@ -432,48 +429,69 @@ def map_analysis() -> Any:
432429
return jsonify(direct_gap_analysis)
433430
abort(404, "No direct overlap found for requested standards")
434431

435-
# First, check if we have cached results in the database
436-
if database.gap_analysis_exists(standards_hash):
437-
gap_analysis_result = database.get_gap_analysis_result(standards_hash)
438-
if gap_analysis_result:
439-
return jsonify(flask_json.loads(gap_analysis_result))
440-
441-
# On Heroku (read-only), check if standards exist before attempting Redis/queue operations
442-
is_heroku = os.environ.get("DYNO") is not None
443-
if is_heroku:
444-
# Check if all requested standards exist
445-
try:
446-
existing_standards = database.standards()
447-
if isinstance(existing_standards, (list, tuple, set)):
448-
existing_lower = {str(s).lower() for s in existing_standards}
449-
missing = [s for s in standards if str(s).lower() not in existing_lower]
450-
if missing:
451-
logger.info(
452-
f"On Heroku: gap analysis request {standards_hash} references "
453-
f"standards that do not exist: {', '.join(missing)}, returning 404"
454-
)
455-
abort(
456-
404, f"One or more standards do not exist: {', '.join(missing)}"
457-
)
458-
except Exception as exc:
459-
# If we can't verify standards, log but don't fail (defensive)
460-
logger.warning(f"Could not verify standards existence on Heroku: {exc}")
461-
462-
# If calculations are disabled, return 404
463-
if os.environ.get("CRE_NO_CALCULATE_GAP_ANALYSIS"):
464-
logger.info(
465-
f"Gap analysis calculations are disabled by CRE_NO_CALCULATE_GAP_ANALYSIS; "
466-
f"refusing to schedule new job for {standards_hash}"
432+
# ----- upstream: cached result -----
433+
cache_key = standards_hash
434+
if database.gap_analysis_exists(cache_key):
435+
cached = database.get_gap_analysis_result(cache_key=cache_key)
436+
if cached:
437+
parsed = json.loads(cached)
438+
if "result" in parsed:
439+
return jsonify({"result": parsed.get("result")})
440+
441+
# ----- upstream: Heroku guard -----
442+
if os.environ.get("HEROKU"):
443+
abort(404, "No such Cache")
444+
445+
# ----- upstream: Redis / RQ path with synchronous fallback -----
446+
db_url = os.environ.get("CRE_CACHE_FILE") or os.environ.get("PROD_DATABASE_URL")
447+
if not db_url:
448+
db_url = str(getattr(getattr(database.session, "bind", None), "url", ""))
449+
try:
450+
conn = redis.connect()
451+
ga_queue_name = os.environ.get("CRE_GA_QUEUE_NAME", "ga")
452+
q = Queue(name=ga_queue_name, connection=conn)
453+
inflight_key = f"ga:inflight:{cache_key}"
454+
inflight_job_id_raw = conn.get(inflight_key)
455+
inflight_job_id = (
456+
inflight_job_id_raw.decode("utf-8")
457+
if isinstance(inflight_job_id_raw, bytes)
458+
else str(inflight_job_id_raw) if inflight_job_id_raw else ""
467459
)
468-
abort(404, "Gap analysis calculations are disabled")
469-
470-
# Now call schedule() which will handle Redis/queue operations
471-
gap_analysis_dict = gap_analysis.schedule(standards, database)
472-
if "result" in gap_analysis_dict:
473-
return jsonify(gap_analysis_dict)
474-
if gap_analysis_dict.get("error"):
475-
abort(404)
476-
return jsonify({"job_id": gap_analysis_dict.get("job_id")})
460+
if inflight_job_id:
461+
try:
462+
inflight_job = job.Job.fetch(id=inflight_job_id, connection=conn)
463+
if inflight_job.get_status() in (
464+
job.JobStatus.QUEUED,
465+
job.JobStatus.STARTED,
466+
):
467+
return jsonify({"job_id": inflight_job_id})
468+
except exceptions.NoSuchJobError:
469+
conn.delete(inflight_key)
470+
471+
j = q.enqueue_call(
472+
description=f"{standards[0]}->{standards[1]}",
473+
func=cre_main.run_gap_pair_job,
474+
kwargs={
475+
"importing_name": standards[0],
476+
"peer_name": standards[1],
477+
"db_connection_str": db_url,
478+
},
479+
timeout=gap_analysis.GAP_ANALYSIS_TIMEOUT,
480+
)
481+
conn.set(inflight_key, str(j.id))
482+
conn.expire(inflight_key, _ga_timeout_seconds())
483+
return jsonify({"job_id": str(j.id)})
484+
except Exception as exc:
485+
logger.warning(
486+
"Redis/RQ unavailable in map_analysis for %s; using synchronous fallback: %s",
487+
cache_key,
488+
exc,
489+
)
490+
try:
491+
return jsonify(_compute_ga_without_redis(database, standards))
492+
except Exception as fallback_exc:
493+
logger.exception("Synchronous GA fallback failed for %s", cache_key)
494+
abort(503, f"Gap analysis unavailable: {fallback_exc}")
477495

478496

479497
@app.route("/rest/v1/map_analysis_weak_links", methods=["GET"])
@@ -1357,4 +1375,4 @@ def import_from_cre_csv() -> Any:
13571375

13581376

13591377
if __name__ == "__main__":
1360-
app.run(use_reloader=False, debug=True)
1378+
app.run(use_reloader=False, debug=True)

0 commit comments

Comments
 (0)