Skip to content

harvester: fail on existing draft - #1019

Merged
kpsherva merged 7 commits into
CERNDocumentServer:masterfrom
kpsherva:harvester-fixed-prod
Nov 4, 2026
Merged

kpsherva merged 7 commits into
CERNDocumentServer:masterfrom
kpsherva:harvester-fixed-prod

Conversation

@kpsherva

@kpsherva kpsherva commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

*closes #1016

@kpsherva
kpsherva force-pushed the harvester-fixed-prod branch from 096d50b to 35fc996 Compare October 8, 2026 18:11
"""
draft_cls = current_rdm_records_service.draft_cls
return list(
draft_cls.get_records_by_parent(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

edit drafts have the same cdsrdm id as the published version. this line loads every draft under the parent so it loads edit drafts of v1, v2, whatever, plus new-version drafts.
and then later at line 123 we only check if the draft id matches the record we're updating (usually latest). so if theres an edit draft on v1 and the harvester is updating v2 (no draft on v2), we still pick up the v1 draft here, then at line 123 it doesnt match v2 so we treat it like a new-version draft, compare it to v2, it looks changed, and we block the harvest even though nobody was editing v2.

we should compare the edit draft to the published version when we are dealing with edit drafts, and only check if there are changes for new version drafts.

@kpsherva kpsherva Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

cool catch - fixed it. Could you recheck the logic in case I misunderstood something?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lgtm


def check(self, stream_entry, *, record=None, record_pid=None, matcher=None):
"""Return an error if the record's files are restricted."""
if record.get("access", {}).get("files") != "restricted":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if there are metadata changes on a record with restricted files, we still stop the harvester from updating even though the restricted files didnt have any changes and werent going to be updated.

is that intentional (restricted files = dont touch the record at all), or should metadata still sync?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is intentional - I don't want to cause the metadata to be out of sync in regard to the files - since the most important part of the record is not synced - let's fail all of it.

Comment on lines +117 to +127
def _fetch_conference(self, url, logger):
"""Fetch the INSPIRE conference metadata, ``None`` on failure."""
try:
response = requests.get(url, timeout=self.timeout)
response.raise_for_status()
return response.json().get("metadata", {})
except (requests.RequestException, ValueError) as e:
logger.warning(
f"Could not fetch INSPIRE conference. | details: url={url}, error={e}"
)
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
def _fetch_conference(self, url, logger):
"""Fetch the INSPIRE conference metadata, ``None`` on failure."""
try:
response = requests.get(url, timeout=self.timeout)
response.raise_for_status()
return response.json().get("metadata", {})
except (requests.RequestException, ValueError) as e:
logger.warning(
f"Could not fetch INSPIRE conference. | details: url={url}, error={e}"
)
return None
_conference_cache = {}
def _fetch_conference(self, url, logger):
"""Fetch the INSPIRE conference metadata, ``None`` on failure."""
if url in ConferenceMapper._conference_cache:
return ConferenceMapper._conference_cache[url]
try:
response = requests.get(url, timeout=self.timeout)
response.raise_for_status()
metadata = response.json().get("metadata", {})
except (requests.RequestException, ValueError) as e:
logger.warning(
f"Could not fetch INSPIRE conference. | details: url={url}, error={e}"
)
metadata = None
ConferenceMapper._conference_cache[url] = metadata
return metadata

just a suggestion: every time we map a paper we fetch the conference from inspire. seen only stops us refetching inside the same paper, so if we harvest a bunch of papers from the same conference we call the same url again and again. maybe we just cache by url so we only fetch each conference once in a harvest

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I plan to ask INSPIRE team to include the conference information inside the JSON format they are generating for us so this code will not be necessary in the future, I would keep it simple. But I think the idea in general is nice

@kpsherva
kpsherva force-pushed the harvester-fixed-prod branch 2 times, most recently from 61501a2 to 4194825 Compare October 12, 2026 14:11
Comment thread app_data/vocabularies/experiments.yaml Outdated
the LHC'
props:
link: http://totem.web.cern.ch/Totem/
- id: H1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should be in a separate PR

Comment on lines +108 to +112
for draft in draft_cls.get_records_by_parent(
record._record.parent, with_deleted=False
):
draft_id = str(draft.pid.pid_value)
is_new_version = not self._is_published(record_cls, draft_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

get_records_by_parent already returns the draft here, so I don't get why we need another DB query via _is_published to get the same PID for all, seems redundant. We can just check if not draft.is_published?

@kpsherva
kpsherva force-pushed the harvester-fixed-prod branch from 41b1a68 to 51fb881 Compare October 15, 2026 17:04
draft_dict = dict(draft)
record_dict = dict(record)
is_edit_draft = str(draft.pid.pid_value) == str(record.pid.pid_value)
# A new version gets its own pids, so only edit drafts are comparable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How this comment is associated with the line below?

record_dict = dict(record)
is_edit_draft = str(draft.pid.pid_value) == str(record.pid.pid_value)
# A new version gets its own pids, so only edit drafts are comparable.
if is_edit_draft and draft_dict.get("pids", {}).get(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: for readability maybe save these in variables and compare e.g draft_doi != record_doi

record_files = self._file_signatures(record)
if is_edit_draft:
return draft_files != record_files
return not draft_files <= record_files

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess here is the new version draft and we are checking if in the new version there are less files than in the published one? Can you explain the reasoning as it is not clear to me?

@kpsherva kpsherva Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The writer uses it to decide whether a user has changed a draft, which blocks the harvest. A new-version draft is created by copying the published record, but its files may not be copied over yet. So an empty
draft, or one with fewer files, is not a user (deliberate) change.

  • Edit draft: it should match the record exactly, so the code compares draft_files != record_files.
  • New-version draft: the code returns not draft_files <= record_files, which is true only if the draft has a file the record lacks, or a file with a different checksum.
  • Fewer files in the new version: the code deliberately does not count this as a change.

So user who deliberately deleted a file from a new version draft would therefore not be detected, but it is a trade-off that this draft will be discarded as probably it should.... since the harvester brings "the latest" state presumably

assert service.read_draft(system_identity, record["id"])


def test_update_discards_unchanged_new_version_draft(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you also check the branch that a new_version exists but it has changes?




def test_update_fails_when_draft_exists(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

similarly here: if there is an edited draft but no changes have been applied, does the harvester complete the update?

content=content,
)
record = current_rdm_records_service.publish(system_identity, draft.id)
add_legacy_recid(add_pid, record, "2882312")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the test claims multiple versions and the latest has restricted files. Here, it seems we create only one version? If so, can you replicate actually multiple ones?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

you are right, the test only created one version, so I changed it to create two: v1 public, and v2 as a report with restricted files.
I removed the duplicate check from _resource_type_versioning because it was not reachable, already handled by validation

@kpsherva
kpsherva force-pushed the harvester-fixed-prod branch from 6ee7acf to 6cf8e97 Compare October 29, 2026 12:06
@kpsherva
kpsherva merged commit db785aa into CERNDocumentServer:master Nov 4, 2026
3 checks passed
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.

harvester: fail if draft exists

4 participants