Skip to content

Commit 21f855a

Browse files
La0Bastien Abadie
and
Bastien Abadie
authored
Check build expiry in Phabricator state update (#109)
Co-authored-by: Bastien Abadie <[email protected]>
1 parent 280ed2b commit 21f855a

File tree

6 files changed

+95
-82
lines changed

6 files changed

+95
-82
lines changed

libmozevent/mercurial.py

+3-19
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import os
1212
import tempfile
1313
import time
14-
from datetime import datetime, timedelta
14+
from datetime import datetime
1515

1616
import hglib
1717
import requests
@@ -340,15 +340,13 @@ def __init__(
340340
queue_name,
341341
queue_phabricator,
342342
repositories,
343-
diff_expiry=timedelta(hours=24),
344343
skippable_files=[],
345344
):
346345
assert all(map(lambda r: isinstance(r, Repository), repositories.values()))
347346
self.queue_name = queue_name
348347
self.queue_phabricator = queue_phabricator
349348
self.repositories = repositories
350349
self.skippable_files = skippable_files
351-
self.diff_expiry = diff_expiry
352350

353351
def register(self, bus):
354352
self.bus = bus
@@ -456,22 +454,8 @@ async def handle_build(self, repository, build):
456454
build,
457455
{"message": error_log, "duration": time.time() - start},
458456
)
459-
if (
460-
build.diff.get("fields")
461-
and build.diff["fields"].get("dateCreated")
462-
and (
463-
datetime.now()
464-
- datetime.fromtimestamp(build.diff["fields"]["dateCreated"])
465-
> self.diff_expiry
466-
)
467-
):
468-
error_log = "This build is too old to push to try repository"
469-
return (
470-
"fail:mercurial",
471-
build,
472-
{"message": error_log, "duration": time.time() - start},
473-
)
474-
elif build.retries:
457+
458+
if build.retries:
475459
logger.warning(
476460
"Trying to apply build's diff after a remote push error "
477461
f"[{build.retries}/{MAX_PUSH_RETRIES}]"

libmozevent/phabricator.py

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
import collections
3+
from datetime import datetime, timedelta
34
import enum
45
import time
56

@@ -13,6 +14,7 @@ class PhabricatorBuildState(enum.Enum):
1314
Queued = 1
1415
Secured = 2
1516
Public = 3
17+
Expired = 4
1618

1719

1820
class PhabricatorBuild(object):
@@ -59,7 +61,9 @@ class PhabricatorActions(object):
5961
Common Phabricator actions shared across clients
6062
"""
6163

62-
def __init__(self, url, api_key, retries=5, sleep=10):
64+
def __init__(
65+
self, url, api_key, retries=5, sleep=10, build_expiry=timedelta(hours=24)
66+
):
6367
self.api = PhabricatorAPI(url=url, api_key=api_key)
6468

6569
# Phabricator secure revision retries configuration
@@ -68,10 +72,12 @@ def __init__(self, url, api_key, retries=5, sleep=10):
6872
self.max_retries = retries
6973
self.retries = collections.defaultdict(lambda: (retries, None))
7074
self.sleep = sleep
75+
self.build_expiry = build_expiry
7176
logger.info(
7277
"Will retry Phabricator secure revision queries",
7378
retries=retries,
7479
sleep=sleep,
80+
build_expiry=build_expiry,
7581
)
7682

7783
# Load secure projects
@@ -114,6 +120,11 @@ def update_state(self, build):
114120
build.revision_url = self.build_revision_url(build)
115121
logger.info("Revision is public", build=str(build))
116122

123+
# Check if the build has not expired
124+
if self.is_expired_build(build):
125+
build.state = PhabricatorBuildState.Expired
126+
logger.info("Revision has expired", build=str(build))
127+
117128
elif retries_left <= 0:
118129
# Mark as secured when no retries are left
119130
build.state = PhabricatorBuildState.Secured
@@ -183,3 +194,31 @@ def build_revision_url(self, build):
183194
Build a Phabricator frontend url for a build's revision
184195
"""
185196
return "https://{}/D{}".format(self.api.hostname, build.revision_id)
197+
198+
def is_expired_build(self, build):
199+
"""
200+
Check if a build has expired, using its Phabricator diff information
201+
Returns True when the build has expired and should not be processed
202+
"""
203+
assert isinstance(build, PhabricatorBuild)
204+
205+
# We need Phabricator diff details to get the date
206+
if build.diff is None:
207+
try:
208+
diffs = self.api.search_diffs(diff_id=build.diff_id)
209+
if not diffs:
210+
raise Exception(f"Diff {build.diff_id} not found on Phabricator")
211+
build.diff = diffs[0]
212+
except Exception as e:
213+
logger.warn("Failed to load diff", build=str(build), err=str(e))
214+
return False
215+
216+
# Then we can check on the expiry date
217+
date_created = build.diff.get("dateCreated")
218+
if not date_created:
219+
logger.warn("No creation date found", build=str(build))
220+
return False
221+
222+
logger.info("Found diff creation date", build=str(build), created=date_created)
223+
224+
return datetime.now() - datetime.fromtimestamp(date_created) > self.build_expiry

tests/conftest.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,12 @@ def _diff_search(request):
129129
if "revisionPHIDs" in params["constraints"]:
130130
# Search from revision
131131
mock_name = "search-{}".format(params["constraints"]["revisionPHIDs"][0])
132+
elif "ids" in params["constraints"]:
133+
# Search from diff IDs
134+
diffs = "-".join(map(str, params["constraints"]["ids"]))
135+
mock_name = "search-{}".format(diffs)
132136
elif "phids" in params["constraints"]:
133-
# Search from diffs
137+
# Search from diff PHIDs
134138
diffs = "-".join(params["constraints"]["phids"])
135139
mock_name = "search-{}".format(diffs)
136140
else:
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"result": {
3+
"data": [
4+
{
5+
"id": 1234,
6+
"type": "DIFF",
7+
"phid": "PHID-DIFF-c0ffee",
8+
"fields": {
9+
"revisionPHID": "PHID-DREV-xxx",
10+
"authorPHID": "PHID-USER-yyy",
11+
"repositoryPHID": "PHID-REPO-zzz",
12+
"refs": [],
13+
"dateCreated": 1510251135,
14+
"dateModified": 1510251135,
15+
"policy": {
16+
"view": "public"
17+
}
18+
},
19+
"attachments": {}
20+
}
21+
],
22+
"maps": {},
23+
"query": {
24+
"queryKey": null
25+
},
26+
"cursor": {
27+
"limit": 100,
28+
"after": null,
29+
"before": null,
30+
"order": null
31+
}
32+
},
33+
"error_code": null,
34+
"error_info": null
35+
}

tests/test_mercurial.py

-61
Original file line numberDiff line numberDiff line change
@@ -917,64 +917,3 @@ def test_get_base_identifier(mock_mc):
917917
assert (
918918
mock_mc.get_base_identifier(stack) == "tip"
919919
), "`tip` commit should be used when `use_latest_revision` is `True`."
920-
921-
922-
@responses.activate
923-
@pytest.mark.asyncio
924-
async def test_push_failure_diff_expiry(PhabricatorMock, mock_mc):
925-
diff = {
926-
"revisionPHID": "PHID-DREV-badutf8",
927-
"baseRevision": "missing",
928-
"phid": "PHID-DIFF-badutf8",
929-
"id": 555,
930-
# a date in 2017
931-
"fields": {"dateCreated": 1510251135},
932-
}
933-
build = MockBuild(4444, "PHID-REPO-mc", 5555, "PHID-build-badutf8", diff)
934-
with PhabricatorMock as phab:
935-
phab.load_patches_stack(build)
936-
937-
bus = MessageBus()
938-
bus.add_queue("phabricator")
939-
940-
from libmozevent import mercurial
941-
942-
mercurial.TRY_STATUS_URL = "http://test.status/try"
943-
944-
sleep_history = []
945-
946-
class AsyncioMock(object):
947-
async def sleep(self, value):
948-
nonlocal sleep_history
949-
sleep_history.append(value)
950-
951-
mercurial.asyncio = AsyncioMock()
952-
953-
responses.get(
954-
"http://test.status/try", status=200, json={"result": {"status": "open"}}
955-
)
956-
957-
repository_mock = MagicMock(spec=Repository)
958-
959-
worker = MercurialWorker(
960-
"mercurial", "phabricator", repositories={"PHID-REPO-mc": repository_mock}
961-
)
962-
worker.register(bus)
963-
964-
await bus.send("mercurial", build)
965-
assert bus.queues["mercurial"].qsize() == 1
966-
task = asyncio.create_task(worker.run())
967-
968-
# Check the treeherder link was queued
969-
mode, out_build, details = await bus.receive("phabricator")
970-
task.cancel()
971-
972-
assert build.retries == 0
973-
974-
assert mode == "fail:mercurial"
975-
assert out_build == build
976-
assert details["duration"] > 0
977-
assert details["message"] == "This build is too old to push to try repository"
978-
979-
# no call sent to TRY_STATUS_URL
980-
assert len(responses.calls) == 0

tests/test_phabricator.py

+12
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,15 @@ def test_backoff(PhabricatorMock):
4949
phab.update_state(build)
5050
assert build.state == PhabricatorBuildState.Public
5151
assert phab.is_visible.call_count == 3
52+
53+
54+
def test_expiry(PhabricatorMock, caplog):
55+
"""
56+
Test diff expiration method on a known API mock
57+
"""
58+
build = MockBuild(1234, "PHID-REPO-mc", 5678, "PHID-HMBT-deadbeef", {})
59+
build.state = PhabricatorBuildState.Queued
60+
build.diff = None # Force loading the mockup
61+
62+
with PhabricatorMock as phab:
63+
assert phab.is_expired_build(build)

0 commit comments

Comments
 (0)