What version of gotd are you using?
v0.135.0
Can this issue be reproduced with the latest version?
Yes
What did you do?
I have an account with hundreds of channels that contain a large volume of unread messages. Every time I log in, the update handler gets completely blocked.
After several days of careful investigation, I discovered a circular wait deadlock in the telegram/updates package. The deadlock occurs during the initialization phase when updateManager.Run() starts.
Steps to reproduce:
- Have an account with a lot of channels (exceeds the internal queue buffer size)
- Ensure these channels have many unread messages
- Call
updateManager.Run() to authenticate and start the update handler
- The handler will permanently block during
getDifferenceLogger()
What did you expect to see?
The update handler should successfully initialize and process all updates from multiple channels, regardless of the number of channels or the volume of unread messages.
What did you see instead?
The update handler enters a permanent deadlock state and becomes completely unresponsive.
Root Cause Analysis
The deadlock is a classic circular wait scenario involving two buffered channels with size 10:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Circular Wait Deadlock │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ internalState.Run() │
│ │ │
│ ├─ getDifferenceLogger() [BLOCKED] │
│ │ └─ getDifference() → handleUpdates() → applyCombined() │
│ │ └─ UpdateChannelTooLong → st.Push() │
│ │ │ │
│ │ ▼ │
│ │ [BLOCKED! Waiting for channelState.updates] │
│ │ │
│ ▼ │
│ for loop (never starts) │
│ └─ case u := <-s.internalQueue [should drain the queue] │
│ │
│ │
│ channelState.Run() (per-channel goroutine) │
│ │ │
│ ├─ getDifference() [BLOCKED] │
│ │ └─ UpdatesChannelDifference → s.out <- update │
│ │ │ │
│ │ ▼ │
│ │ [BLOCKED! Waiting for internalQueue] │
│ │ │
│ ▼ │
│ for loop (never starts) │
│ └─ case u := <-s.updates [should consume updates] │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Deadlock Chain Explanation
internalState.Run() calls getDifferenceLogger() before entering its event loop
getDifference() may recursively call itself when processing UpdatesDifferenceSlice
- During
applyCombined(), UpdateChannelTooLong updates trigger channelState.Push()
channelState.Push() tries to send to channelState.updates (buffer=10)
- If ≥11 channels exist, the 11th push blocks because the buffer is full
channelState.Run()'s for loop (which would drain updates) hasn't started yet
- Because
channelState.Run() is blocked in its own getDifference() call
channelState.getDifference() tries to send to internalQueue (buffer=10)
- If multiple channels do this simultaneously,
internalQueue fills up and blocks
internalState.Run()'s for loop (which would drain internalQueue) hasn't started yet
- We're back to step 1 → DEADLOCK
Key Code Locations
telegram/updates/state.go:82-83 - internalQueue: make(chan tracedUpdate, 10)
telegram/updates/state.go:167 - getDifferenceLogger() called before for loop
telegram/updates/state_apply.go:60 - st.Push() can block
telegram/updates/state_channel.go:62 - updates: make(chan channelUpdate, 10)
telegram/updates/state_channel.go:100 - getDifference() called before for loop
telegram/updates/state_channel.go:260 - Send to s.out (which is internalQueue) can block
Temporary Workaround
Increasing the buffer sizes of both channels resolves the issue:
// In telegram/updates/state.go
internalQueue: make(chan tracedUpdate, 1000), // was 10
// In telegram/updates/state_channel.go
updates: make(chan channelUpdate, 100), // was 10
After testing with increased buffer sizes, the deadlock no longer occurs.
What Go version and environment are you using?
go version go1.24.4 darwin/arm64
go env Output
$ go env
Additional Notes
The fundamental issue is the startup dependency cycle:
internalState.Run waits for getDifferenceLogger to complete
getDifferenceLogger waits for channelState.Push to complete
channelState.Push waits for channelState.Run's for loop
channelState.Run waits for getDifference to complete
getDifference waits for internalState.Run's for loop
What version of gotd are you using?
v0.135.0
Can this issue be reproduced with the latest version?
Yes
What did you do?
I have an account with hundreds of channels that contain a large volume of unread messages. Every time I log in, the update handler gets completely blocked.
After several days of careful investigation, I discovered a circular wait deadlock in the
telegram/updatespackage. The deadlock occurs during the initialization phase whenupdateManager.Run()starts.Steps to reproduce:
updateManager.Run()to authenticate and start the update handlergetDifferenceLogger()What did you expect to see?
The update handler should successfully initialize and process all updates from multiple channels, regardless of the number of channels or the volume of unread messages.
What did you see instead?
The update handler enters a permanent deadlock state and becomes completely unresponsive.
Root Cause Analysis
The deadlock is a classic circular wait scenario involving two buffered channels with size 10:
Deadlock Chain Explanation
internalState.Run()callsgetDifferenceLogger()before entering its event loopgetDifference()may recursively call itself when processingUpdatesDifferenceSliceapplyCombined(),UpdateChannelTooLongupdates triggerchannelState.Push()channelState.Push()tries to send tochannelState.updates(buffer=10)channelState.Run()'s for loop (which would drainupdates) hasn't started yetchannelState.Run()is blocked in its owngetDifference()callchannelState.getDifference()tries to send tointernalQueue(buffer=10)internalQueuefills up and blocksinternalState.Run()'s for loop (which would draininternalQueue) hasn't started yetKey Code Locations
telegram/updates/state.go:82-83-internalQueue: make(chan tracedUpdate, 10)telegram/updates/state.go:167-getDifferenceLogger()called before for looptelegram/updates/state_apply.go:60-st.Push()can blocktelegram/updates/state_channel.go:62-updates: make(chan channelUpdate, 10)telegram/updates/state_channel.go:100-getDifference()called before for looptelegram/updates/state_channel.go:260- Send tos.out(which isinternalQueue) can blockTemporary Workaround
Increasing the buffer sizes of both channels resolves the issue:
After testing with increased buffer sizes, the deadlock no longer occurs.
What Go version and environment are you using?
go version go1.24.4 darwin/arm64
go envOutputAdditional Notes
The fundamental issue is the startup dependency cycle:
internalState.Runwaits forgetDifferenceLoggerto completegetDifferenceLoggerwaits forchannelState.Pushto completechannelState.Pushwaits forchannelState.Run's for loopchannelState.Runwaits forgetDifferenceto completegetDifferencewaits forinternalState.Run's for loop