Skip to content

Support per-document read lock - #147

Open
msujew wants to merge 4 commits into
mainfrom
msujew/refactor-read-lock
Open

msujew wants to merge 4 commits into
mainfrom
msujew/refactor-read-lock

Conversation

@msujew

@msujew msujew commented Aug 26, 2026

Copy link
Copy Markdown
Member

This PR attempts to fix a similar problem as our implementation already does inside of Langium. Some services require only specific document states to be available. For example, a document symbol request can be immediately resolved, once the document has been succesfully parsed - without waiting for the build to finish, or even references to be linked.

This change adds two main features for this:

  1. Adds a ReadAt method to the Lock interface, which allows to read documents at specific states.
  2. Adds a DocumentStateRequirements interface, that LSP services can (optionally) implement to start reading at a specific workspace/document state. Any service that does not implement this interface falls back to Read.

Removes the downgrade functionality from the lock, which has been replaced with ReadAt. Write and Read are now fully exclusive.

Related to eclipse-langium/langium#2214.

@msujew
msujew requested a review from spoenemann August 26, 2026 14:48
@msujew
msujew force-pushed the msujew/refactor-read-lock branch from 836ce5c to 59cdf87 Compare August 26, 2026 14:53
Comment thread server/completion_contributor.go
Comment thread workspace/builder.go
Comment thread workspace/lock_test.go Outdated
@msujew
msujew force-pushed the msujew/refactor-read-lock branch 2 times, most recently from 4a1f066 to 2fd5280 Compare October 5, 2026 09:51
@msujew
msujew requested a review from Lotes October 5, 2026 09:57
@msujew
msujew force-pushed the msujew/refactor-read-lock branch from 2fd5280 to fd458d0 Compare October 5, 2026 10:07
@msujew
msujew force-pushed the msujew/refactor-read-lock branch 4 times, most recently from d251b46 to 48037ae Compare October 14, 2026 12:46
@msujew

msujew commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

@Lotes I identified a few more issues in the Builder default implementation through some of your feedback. Symbol collection (local and imported) and linking is now split into separate phases.

@msujew
msujew force-pushed the msujew/refactor-read-lock branch 2 times, most recently from 978c1e2 to 57447e2 Compare October 22, 2026 11:20
^ Conflicts:
^	server/server.go
^	workspace/builder.go

^ Conflicts:
^	cmd/fastbelt/generate.go
^	workspace/builder.go
@msujew
msujew force-pushed the msujew/refactor-read-lock branch from 57447e2 to a59d2bf Compare October 22, 2026 13:37

@sailingKieler sailingKieler left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you Mark! I like this approach much better, never fully understood the downgrade idea.

I have a couple of hints for better naming & documentation and one critical question in the last remark, see below. Hope this helps.

Comment thread document.go
//
// It is safe to call concurrently with a running build: the atomic load pairs
// with [Document.SetState], so a caller that observes a state bit is
// guaranteed to see all document data written by that build phase.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A nice AI generated comment 😄

, so a caller that observes a state bit is guaranteed to see all document data written by that build phase.

This is rather an assertion the impl hopefully satisfies instead of a consequence.
I suggest to replace , so a by . A.

Comment thread workspace/builder.go
// [Lock.ReadAt] requests are admitted as soon as the states they need are reached,
// and report the workspace-wide floor after each phase barrier. The floor relies
// on every document outside docs already being complete, which the caller must
// ensure (the updater collects every incomplete document into the build set).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doc is very AI compact. Not sure whether I really get it or rather project my expectation onto the statement...

Comment thread workspace/builder.go
}
if ctx.Err() != nil {
return
if ctx.Err() != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ctx.Err() != nil usually indicates (self) cancellation or the current run, correct?

I think that is worth a comment at the beginning of the method.

Comment thread workspace/builder.go
Comment thread workspace/builder.go
if err := ctx.Err(); err != nil {
return err
}
// Phase 2 barrier: the whole workspace has its static scopes computed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// Phase 2 barrier: the whole workspace has its static scopes computed.
// Phase 2 barrier release: the whole workspace has its static scopes computed.

Comment thread workspace/lock.go
writeHeld bool // exclusive write phase is active
writeWaiters int // number of goroutines waiting to acquire the write lock
readers int // number of active shared lock holders (Read and ReadAt)
stateAdvanced bool // current write entered the build phase; document states only advance

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe

Suggested change
stateAdvanced bool // current write entered the build phase; document states only advance
stateAdvancing bool // current write entered the build phase; document states only advance

Comment thread workspace/lock.go
mu sync.Mutex
cond *sync.Cond
writeHeld bool // exclusive write phase is active
writeWaiters int // number of goroutines waiting to acquire the write lock

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

despite the symmetry to writeHeld writeWaiters is a weird word, waiters work in restaurants.

Suggested change
writeWaiters int // number of goroutines waiting to acquire the write lock
waitingWriters int // number of goroutines waiting to acquire the write lock

Comment thread workspace/lock.go

mu sync.Mutex
cond *sync.Cond
writeHeld bool // exclusive write phase is active

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe just writing

Suggested change
writeHeld bool // exclusive write phase is active
writing bool // exclusive write phase is active

Comment thread workspace/lock.go
}()

do(ctx, downgrade)
do(ctx)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My gut feeling says that some

    if ctx.Err() != nil {
        // current write task is canceled
		return
	}
    do(ctx)

would complete the disposal of older write jobs, although this is done within do, at least by the builder.
At least it documents how abortion of pending writers is actually performed.

Comment thread workspace/initializer.go
if len(docs) == 0 {
return nil
}
return builder.Build(ctx, docs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Critical: Why is this call of builder.Build(ctx, docs) not protected by a write lock?

Early didChange and documentSymbols requests by lang clients may race with the initial workspace build, don't they?

Besides in Langium we have the workspace ready promise

I think we should have <-chan struct{} singleton instance serving the same purpose.

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.

3 participants