Skip to content

[ENG-9044] Add manage command to resync preprint dois v1 - #11617

Open
Vlad0n20 wants to merge 13 commits into
CenterForOpenScience:feature/pbs-26-19from
Vlad0n20:fix/ENG-9044
Open

Vlad0n20 wants to merge 13 commits into
CenterForOpenScience:feature/pbs-26-19from
Vlad0n20:fix/ENG-9044

Conversation

@Vlad0n20

@Vlad0n20 Vlad0n20 commented Mar 2, 2026 •

Copy link
Copy Markdown
Contributor

Ticket

Purpose

Changes

Side Effects

QE Notes

CE Notes

Documentation

@cslzchen cslzchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good overall. In addition to my questions/comments:

  • Can we add logs of the output of the logs for you local run?
  • We should also work with CE to test this command with a copy of production DB.

Comment thread osf/management/commands/resync_preprint_dois_v1.py Outdated
Comment thread osf/management/commands/resync_preprint_dois_v1.py Outdated
Comment thread osf/management/commands/resync_preprint_dois_v1.py Outdated
Comment thread osf/management/commands/resync_preprint_dois_v1.py Outdated
Comment thread osf/management/commands/resync_preprint_dois_v1.py Outdated
@Vlad0n20 Vlad0n20 changed the title Add manage command to resync preprint dois v1 [ENG-9044] Add manage command to resync preprint dois v1 Mar 9, 2026

@cslzchen cslzchen left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good 👍

  • Make sure we test this against a copy of prod DB before running this on prod.
    • If there are errors, need look further on why they fail, and probably another fix.
    • Test if the default batch size is good, how long does it take, and also see how many times we need to repeat this.
  • Make sure whoever uses this command knows that we need to repeatedly run this command.

Comment on lines +113 to +117
help=(
'Maximum number of preprints to process per run (default: 500). '
'The command processes the first N eligible preprints and exits; '
're-run the command to continue with the next batch.'
),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oh, I see. This is an OK alternative to the loop I suggested. Just need to make sure whoever runs this command is aware of the fact that they need to keep running this command until none exists. Cc @adlius for your input on this.

@adlius
adlius changed the base branch from feature/pbs-26-2 to feature/pbs-26-6 April 7, 2026 13:13
@Vlad0n20
Vlad0n20 force-pushed the fix/ENG-9044 branch 2 times, most recently from e803a0c to e1d4931 Compare April 14, 2026 11:46
@antkryt

antkryt commented May 4, 2026

Copy link
Copy Markdown
Contributor

LGTM

@mkovalua mkovalua left a comment •

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.

LGTM, @Vlad0n20, please change target branch to current one feature/pbs-26-9, I have no permission to do it

@Vlad0n20
Vlad0n20 changed the base branch from feature/pbs-26-6 to feature/pbs-26-9 May 13, 2026 09:02
@Vlad0n20
Vlad0n20 changed the base branch from feature/pbs-26-9 to feature/pbs-26-15 July 28, 2026 14:13
Comment thread api/users/views.py Outdated

user.date_last_logged_in = timezone.now()
user.external_identity[provider][provider_id] = 'VERIFIED'
if provider.lower() in OSFUser.SOCIAL_FIELDS:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this part of this ticket?

@adlius adlius left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So I have a couple of suggestions. Since Crossref has a limit of 10000 submissions from a single user, might as well set the default batch size to 1000 and no rate limit. Instead let's run the task every five minutes so that we can send about 12000 requests per hour.

Also there are some commits from other branches that is in this PR.

Comment thread osf_tests/test_user.py Outdated
assert len(user.system_tags) == 1

tag = Tag.all_tags.get(name=tag_name, system=True)
tag = Tag.all_tags.get(name=tag_name.lower(), system=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here. Is this part of this ticket?

return qs


def resync_preprint_dois_v1(dry_run=True, batch_size=500, rate_limit=100, provider_id=None):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The rate limit of 100 is perhaps to restrictive. According to conversations with Crossref, they have a limit of 10000 pending items for each user.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A 500 batch size is also perhaps too small.


if rate_limit and not record_number % rate_limit:
logger.info(f'Rate limit reached at {record_number} preprints, sleeping {RATE_LIMIT_SLEEP}s')
time.sleep(RATE_LIMIT_SLEEP)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am afraid this would clog the celery worker when this is run as a celery beat task.

@brianjgeiger brianjgeiger left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Drive by:

  1. What queue are the tasks going onto? The default queue? We should probably ensure that they go on the Low Priority queue to keep from clogging the pipes too much.
  2. If we have a cron job that happens every five minutes, how are you going to run this in a dry-run scenario to make sure it's going to work before the cron job runs? We might want to rethink our strategy here.

@adlius adlius left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Some issues found during local testing.

Comment thread website/identifiers/clients/crossref.py

@adlius adlius left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Some more issues to fix.

def create_identifier(self, preprint, category, include_relation=True):
if category == 'doi':
metadata = self.build_metadata(preprint, include_relation)
metadata = self.build_metadata(preprint, include_relation, include_unversioned_doi=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One more issue found during local testing: on an unmoderated preprint provider, when a user creates a new version but never publish that version, build_unversioned_posted_content would read preprint.get_guid().referent, which hands back that unpublished draft instead of the preprint actually being deposited. This is problematic because the unpublished draft will have date_published to be None and that would fail when building the payload.

Comment thread api/crossref/views.py
@@ -48,6 +49,23 @@ def post(self, request):
if record.get('status').lower() == 'success' and doi:
msg = record.find('msg').text
created = bool(msg == 'Successfully added')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So a DOI in the doi_unversioned category is only written when Crossref replies Successfully added. This could be a potential issue because if the DOI
is already registered, the email probably comes back with something like Successfully updated, so for those preprints
this branch never runs.

This would affect legacy preprints that exists before versioning was a thing. Their base guid is their original guid, so the DOI deposit here is the one registered years ago.

I checked against a local copy of prod db. One of preprint 22hq2 has 10.31219/osf.io/22hq2 already registered with crossref. So it's possible that the crossref response email wouldn't contain Successfully added but Successfully updated or some other language. Will need to confirm if that is indeed the case.

If that is the case, one possible fix is that for an unversioned DOI, store doi_unversioned on any successful record rather than only on Successfully added.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There is another problem: if we run the resync task every five minutes, and if the preprint that was resynced in the last run still haven't received an email back confirming the update of the DOI, then in the next run it will be included in the resync task again. So we'd have to rethink how we should do this. One idea suggested by @cslzchen is that we follow the example in this PR and monitory the number of active tasks and spawn new tasks if the number of active task falls below a certain number (say 1000). In this case, we don't have to worry about running the task every five minutes.

).exclude(
tags__name='qatest',
tags__system=True,
).select_related('provider').distinct()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we probably need an order_by here otherwise the ordering of the queryset would be undeterministic.

@adlius
adlius changed the base branch from feature/pbs-26-15 to feature/pbs-26-18 September 22, 2026 16:08
@cslzchen
cslzchen self-requested a review September 29, 2026 14:56
@cslzchen
cslzchen changed the base branch from feature/pbs-26-18 to feature/pbs-26-19 September 29, 2026 14:56
Comment thread api/crossref/views.py
).first()
if v1_preprint:
v1_preprint.set_identifier_value(category='doi_unversioned', value=doi)
Preprint.objects.filter(id=v1_preprint.id).update(doi_resync_queued_at=None)

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.

PR comment: Added doi_resync_queued_at, so not include them to eligible for resync

Comment thread api/crossref/views.py Outdated
_, version = Guid.split_guid(guid) if guid else (None, None)
if not version:
logger.info(f'Unversioned DOI confirmed by CrossRef: {doi}')
if created and guid:

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.

PR comment: removed created so preprints which have updated status have also been processed

Comment thread website/identifiers/clients/crossref.py Outdated
return posted_content

def build_unversioned_posted_content(self, preprint, element):
latest = preprint.get_guid().referent

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.

PR comment: removed referent which hands back that unpublished draft instead of the preprint actually being deposite



@dataclass
class ResyncPassResult:

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.

PR comments: added helper methods.


try:
async_request_identifier_update.apply_async(kwargs={'preprint_id': preprint._id})
Preprint.objects.filter(id=preprint.id).update(doi_resync_queued_at=timezone.now())

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.

PR comment: set doi_resync_queued_at so the preprint be marked and we know it is processing


try:
async_request_identifier_update.apply_async(kwargs={'preprint_id': preprint._id})
Preprint.objects.filter(id=preprint.id).update(doi_resync_queued_at=timezone.now())

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.

PR comment: set doi_resync_queued_at so the preprint be marked and we know it is processing

return max(0, self.total - self.queued)


def get_in_flight_doi_resync_count():

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.

PR comment: The count of the preprints is being processed right now

Comment thread api/crossref/views.py
elif 'possible preprint/vor pair' not in msg.lower():
# Directly updates the identifier
preprint.set_identifier_value(category='doi', value=doi)
Preprint.objects.filter(id=preprint.id).update(doi_resync_queued_at=None)

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.

PR comment: set doi_resync_queued_at to None when preprint is being processed

continue

try:
async_request_identifier_update.apply_async(kwargs={'preprint_id': preprint._id})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How is this line different from line 123? If line 123 submits a doi record for the v1 version, then this would also submit a doi record for the v1 version, right? Doesn't that mean the unversioned DOI would now associate with the earliest version instead of the latest version? Am I missing something here?

Comment thread website/identifiers/clients/crossref.py Outdated
body.append(self.build_posted_content(preprint, element, include_relation))

if include_unversioned_doi:
body.append(self.build_unversioned_posted_content(preprints[0], element))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is related to the previous comment: if we always use preprints[0] for the include_unversioned_doi, then that means we would use whatever preprint that gets passed to this function for the unversioned doi, instead of the latest version.

f'Skipping preprint {preprint._id}: '
f'provider {preprint.provider._id} has no DOI prefix'
)
skipped += 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would it actually get skipped? Cuz it seems that the queryset never excludes preprints whose provider doesn't have a doi_prefix, resulting in the same preprint getting queued up and skipped again and again without actually skipping.

Comment thread website/identifiers/clients/crossref.py Outdated
versioned_guids__version__lt=preprint.version,
include_rejected=False,
)
) if include_relation else []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is where I don't quite understand: what does include_relation means and why we only care about preprint_versions when include_relation is true? We seem to be adding new meaning to the include_relation flag here, which seems to be only meant for whether there is an article_doi to the preprint.

@adlius adlius left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit picks.


minted_doi = preprint.get_identifier_value('doi')
doi = minted_doi or self.build_doi(preprint)
if minted_doi and minted_doi == self.build_unversioned_doi(preprint):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There are some preprints on OSF whose stored doi's prefix is different from the provider's doi_prefix. Need some guard here to handle the edge case.

Comment thread api/crossref/views.py
if legacy_doi:
legacy_doi.remove()

elif record.get('status').lower() == 'failure':

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What if a preprint fails? Would it result in an infinite loop where the failed preprint gets queued again?

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.

7 participants