Skip to content

Periodically find and queue missing compiler compatibility checks #804

Open

Description

@thomashoneyman

A package can finish publishing, but some of its compiler compatibility checks may never get queued. For example, the server might time out or restart while finding the checks to run. The package is available to users, but its list of supported compilers can remain incomplete indefinitely.

This happened with web-storage@5.0.1 in #800.

#801 makes follow-up scheduling faster and inserts jobs one at a time. That saves jobs already queued before an interruption, but it cannot recover checks it never reached. #803 improves solver performance, but faster solving alone does not repair missing checks.

So the basic problem remains: how do we detect and fix the unavoidable case where a compiler matrix job might not get queued for a published package? And how do we do it without introducing new state?

I think we can accomplish this with a regular, stateless sweep over the registry. The basic idea is to run a small repair command on a regular interval, for example through a systemd timer. Each run starts fresh and asks: Which published package versions still need compiler checks?

For each published package version and each supported compiler:

  1. If the package metadata already lists that exact compiler, skip it.
  2. If a matrix job already exists for that package version and compiler, skip it.
  3. Otherwise, try to find dependency versions that allow a build with that compiler.
  4. If a build plan is found, immediately insert a matrix job into the existing queue.
  5. If no plan is found, leave it alone and try again on a later sweep. A dependency may gain compiler support in the meantime.

The normal server executor still runs the compilation jobs and updates package metadata after successful compilation. The repair command does not compile packages or write compatibility metadata itself.

This survives interruptions itself. As a concrete example, suppose a sweep finds 100 missing checks and stops after queuing 40. The next sweep starts over, skips those 40 existing jobs, and finds the remaining checks. We do not need to remember where the previous sweep stopped.

This also finds old gaps, not just gaps caused by the latest publication or compiler release. It should examine all currently published versions, not only the newest version of each package.

The goal is eventual repair: as sweeps and compilation jobs complete, missing compatibility results are filled in wherever a build succeeds. A missing compiler entry is not proof that the package cannot compile with it, and a solved build plan is not proof of compatibility until compilation succeeds.

Side note: separating queueing from publication success

If publication finishes successfully, a later timeout while queuing compatibility checks currently makes the publish job fail. This is kind of misleading though; the compiler matrix is an eventually-correct best-effort thing, not a requirement for publishing. Compatibility scheduling should be outside the publication job's success/failure boundary and timeout. The periodic sweep can take responsibility for discovering that follow-up work.

The same principle applies to matrix jobs: once compilation succeeds and the compatibility metadata is successfully written/pushed, a later failure to schedule dependant checks should not turn that successful check into a failed job.

Activity

  1. mikesol commented on Oct 14, 2026

    @mikesol

    Thanks for writing this up! I think the stateless sweep is the right call. Two small things. First, could we say that a finished job counts as "already exists" in step 2, even if it failed? Otherwise, if I understand correctly, a package that fails to compile gets re-queued on every sweep. Second, packages with no build plan get re-solved on every sweep. My understanding is that #803 makes this cheap, but it'd still be good to measure a full sweep and adjust the interval accordingly. Nice work!

  2. thomashoneyman commented on Oct 14, 2026

    @thomashoneyman
    MemberAuthor

    Yeah, agreed on both. A job counts as “already exists” whether it’s pending, succeeded, or failed. The sweep would only add checks that never got a job in the first place (it won't retry failed compilations) — I can make that more explicit.

    On the other hand, packages with no build plan indeed get re-solved on every sweep. I feel like that's the biggest weakness in this proposal, and I wasn't sure how bad that would be so I grabbed the current registry data and ran the equivalent of one of these sweeps over it. On a machine with a little (not much) more firepower than the registry machine, it took about 12 minutes to run and it repeated about 120,000 unsuccessful solves, so that feels like a dealbreaker.

    I took a stab at an implementation so I could do that sweep and it feels pretty good minus the repeated solves. Two options come to mind:

    1. We can use our Cache effect to put a hash of the solver inputs on disk for a given compatibility check, such that on future re-solves it's just loaded instead of actually solved. But this doesn't help with avoiding going through those 120k (and growing) entries, it just skips solving work altogether.
    2. We can piggyback this onto the jobs statuses directly, such that failing to solve inserts a failed compilation check job without actually independently processing the job, so we just record that we couldn't solve -- and we record the solver inputs, for debugging. We'd have this as status "no build plan" and then none of the 120k would be loaded on the next sweep.

    This all assumes we're still happy considering no build plan to be a terminal outcome and we're not promising to keep re-checking all these packages as new dependency versions are published within their bounds.

  3. thomashoneyman commented on Oct 14, 2026

    @thomashoneyman
    MemberAuthor

    Well, I guess this has problems too -- we require jobs to have a build plan, and this wouldn't have one. So then we either relax that requirement to allow null, where null must always correspond with a failed job, or we decide to store this elsewhere.

  4. f-f commented on Oct 14, 2026

    @f-f
    Member

    I have been looking at this for the past few days and I am not really happy about it - some notes:

    • the whole writeup seems to stem from "the jobs are being terminated for the wrong reason so we need to fix the situation up afterwards", which doesn't sound like the best premise possible? In the sense that if the jobs are being unjustifiably terminated we should try to figure out how to not terminate them, or how to rework them in a way that doesn't lead to timeouts
    • that is all to say that I am happy to have a "fixup" script that we run once to get things on track/migrate, but we should not be running it periodically just because the current queueing implementation has a flaw. We should fix the implementation!
    • I am not sure why the proposal mentions systemd. We already have a system in place to start periodic jobs, and everything should use that, or we migrate everything to something else. Having a sprawl of different mechanisms for starting periodic registry jobs is less maintainable.
    • now for the actual problem: my understanding of the issue is this:
      1. there is a publish job
      2. the publish happens, and there are a million matrix jobs to queue
      3. the job times out and we don't queue all of them
    • if my understanding is accurate then I can think of a few solutions that we could apply that are more reasonable than a fixup job:
      1. we split the publish (which guarantees compat with one compiler) from the "we need to queue compiler-compat matrix jobs" and the "we need to queue dependents-compat matrix jobs" tasks; that is, we introduce two new kinds of jobs, that have a longer timeout than the publish ones. In this way publishing can go through, and the matrix-job-enqueueing can take its time
      2. or, we keep these in the same publish job, but have a timeout for the publish part (isn't 10 mins too short btw? compilation can take a long time) and a different timeout for the matrix-queueing part. Less sure how that would be wired up, but I have the feeling that it's less setup than the previous alternative (no new DB tables, etc)
    • aside: the current design of the matrix-job-recursive-queueing thing is built on the fact that we do not store failed solver failure for packages. This is because a package X at version N might be failing to solve for all M versions that try to queue its matrix job, and succeed at the M+1 that makes it compatible with a certain compiler. So if we are to do that we would need to rethink that whole thing (it was a lot of sweat to get right. Maybe Astra can do better, but I am not confident about that)
    • aside 2: we should revert the "preserve queued work" part of Reduce matrix scheduling time and preserve queued work #801: if we don't manage to solve everything in the generous allotted time (we can have a 2h timeout even) we should fail the whole job loudly, so we can go back and have a look at why this fails. If we enqueue partial work then failure is less noticeable and we are incentivised to have things like the fixup job described here
    • about failing jobs loudly: implementation effort spent in warning us more effectively of failures has a very high ROI and should be prioritised over other improvements
  5. thomashoneyman commented on Oct 14, 2026

    @thomashoneyman
    MemberAuthor

    the whole writeup seems to stem from "the jobs are being terminated for the wrong reason so we need to fix the situation up afterwards"

    I'd rephrase this as: matrix jobs can fail to start for a variety of reasons, some of which are unavoidable. In this case, it was timeouts; in another, maybe the machine goes down or restarts. I agree we should first try and make it so these situations are rare, and #801 and #803 help ensure that we don't hit the case that originally triggered all these misses. But it's not completely avoidable, so it still feels worth seeing if we can have some periodic job to check for dropped jobs and re-enqueue them.

    I am not sure why the proposal mentions systemd. We already have a system in place to start periodic jobs

    Maybe we can use the existing system, in which case I agree with you! When I was originally looking for failed jobs I was looking at the database directly, and that led me to think this fixup likely should run on the registry machine itself — as opposed to those periodic jobs you linked, which are run in GitHub Actions and then submit jobs over the API. So that's the deciding factor IMO: if it must be run on-machine to see the information it needs, then systemd; otherwise, the external job-starter would be better.

    we split the publish (which guarantees compat with one compiler) from the "we need to queue compiler-compat matrix jobs" and the "we need to queue dependents-compat matrix jobs" tasks; that is, we introduce two new kinds of jobs, that have a longer timeout than the publish ones. [...] or, we keep these in the same publish job, but have a timeout for the publish part (isn't 10 mins too short btw?) [...] if we don't manage to solve everything in the generous allotted time (we can have a 2h timeout even) we should fail the whole job loudly

    I'm cautiously optimistic about splitting publish from the follow-on tasks, I think that's probably a nicer architecture. On the other hand, I'm really hesitant to do something like put a 2h timeout in place. That would block the registry for up to 2hrs on a single job, which feels unacceptable. Imagine running spago publish and your job queues for hours before it can be picked up!

    Perhaps with both we could (a) split publish from the follow-on tasks, and (b) move those follow-on computations like solving and matrix cascade into a worker thread where if it happened to take 2hrs, that's OK, it's not blocking subsequent publishes.

    I don't remember exactly how we chose 10 minutes, but IIRC we looked at the slowest compilation in the registry when we did a giant publish of lots of packages and even unicode and deku (the slowest iirc) were well under this.

    about failing jobs loudly: implementation effort spent in warning us more effectively of failures has a very high ROI and should be prioritised over other improvements

    I do like being loudly notified of failures! Alas, currently, no failures in the registry notify us at all, as we don't have any real monitoring besides the health of the machine itself. True, if we don't even fail loudly in the first place then we wouldn't be notified either so they're both in play, but a loud failure today would probably go unnoticed too if it's just in logs.

  6. f-f commented on Oct 14, 2026

    @f-f
    Member

    I'd rephrase this as: matrix jobs can fail to start for a variety of reasons, some of which are unavoidable. In this case, it was timeouts; in another, maybe the machine goes down or restarts.
    But it's not completely avoidable, so it still feels worth seeing if we can have some periodic job to check for dropped jobs and re-enqueue them.

    I am not convinced yet that it's not avoidable 🙂
    For the publishing job we have numerous checks in place so that, if for some reason the publish job was dropped halfway through, then we'd figure out the second time around where we were with it and pick it back up. Couldn't we do the same with the matrix-enqueueing job? (assuming that we split it in its own separate job type)
    I.e. once it's its own job type, then queueing becomes a near-instant operation (that we can issue before marking the publish job as done, so that if the server restarts the operation can be repeated; once it's an independent job then it's basically an idempotent operation once we revert back to enqueueing all the jobs at once when the solving is done.

    So that's the deciding factor IMO: if it must be run on-machine to see the information it needs, then systemd; otherwise, the external job-starter would be better.

    Not really - the trigger can still be external, then the server code can run on the machine all the checks it needs.

    move those follow-on computations like solving and matrix cascade into a worker thread where if it happened to take 2hrs, that's OK, it's not blocking subsequent publishes

    We need them to be in the queue due to the lock on the git repo. We chose 10 mins because it's a long enough time to achieve stuff and compile and so on, while being short enough that a publish waiting 10 mins while a job is running is not a big deal.
    I guess we can keep the timeout to 10 mins but try to be more granular in what the jobs do, and eventually we can get to the point where nothing times out.

    Alas, currently, no failures in the registry notify us at all, as we don't have any real monitoring besides the health of the machine itself. True, if we don't even fail loudly in the first place then we wouldn't be notified either so they're both in play, but a loud failure today would probably go unnoticed too if it's just in logs.

    Yeah I think we should be worried about getting that stuff in place before we try to rearchitecture stuff 😄

  7. thomashoneyman commented on Oct 14, 2026

    @thomashoneyman
    MemberAuthor

    I.e. once it's its own job type, then queueing becomes a near-instant operation [...]
    Not really - the trigger can still be external, then the server code can run on the machine all the checks it needs.

    OK, this is true, if we're willing to make these kinds of things jobs. I guess i don't have a clear picture of what is meaningful enough a unit to be considered a "job". I've been thinking of that as like, the first-class registry operations (publish, unpublish, ...). How would you describe the new list of job types as you're thinking about this?

  8. f-f commented on Oct 14, 2026

    @f-f
    Member

    I've been thinking of that as like, the first-class registry operations (publish, unpublish, ...). How would you describe the new list of job types as you're thinking about this?

    We can split the jobs however granular we think we need them, and the main constraint we have is due to the lock we need to keep on the consistency of the git repo. The publish job can't really be split much further due to the fact that we need to do a bunch of stuff before we get to a point where we can write in the repo. Stuff that happens after we are done writing to the repo can be split. E.g. in the case of the publishing job we could have the docs publishing (Pursuit upload today, registry docs tomorrow) happen in a subsequent job that gets enqueued at the tail of the publishing job.

    Splitting jobs is added ceremony, but it buys us the ability to interleave more urgent stuff in between and ultimately having the registry be more responsive. E.g. another publish job could slip in between a publish operation and its docs publishing.

    This are the job kinds we have today:

    data Job
    = PublishJob PublishJobData
    | UnpublishJob UnpublishJobData
    | TransferJob TransferJobData
    | MatrixJob MatrixJobData
    | PackageSetJob PackageSetJobData

    I am proposing that we add EnqueueCompilerJobsForPackage and EnqueueDependantsForPackage jobs to the list. We'd enqueue those from any place where we call solveForAllCompilers and solveDependantsForCompiler respectively, and never from the web API (just like matrix jobs today). The jobs would do little more than just call these functions really. It is important that we revert to adding things to the DB all at once after solving because this gives us the ability to rerun the job idempotently (by solving, checking if those solving jobs have been added to the DB, and enqueue them if not).
    If you'd like to add a recurring script that enqueues matrix jobs on demand (which I don't think we should do, because as I described above matrix jobs should be enqueued only when they solve) then we'd need to expose an endpoint to enqueue a single matrix job from the API.

  9. thomashoneyman commented on Oct 14, 2026

    @thomashoneyman
    MemberAuthor

    OK, I'm down with this. I am happier without the recurring job, and I don't mind adding more granular jobs (the extra ceremony there would be more complexity elsewhere anyway). Thanks for clarifying.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions