Skip to content

Commit 2847ef3

Browse files
rl-gitpodroboquat
authored andcommitted
[ws-proxy] Use instance ID to not stomp on newer workspaces when handling events
1 parent 74816df commit 2847ef3

File tree

2 files changed

+886
-23
lines changed

2 files changed

+886
-23
lines changed

components/ws-proxy/pkg/proxy/infoprovider.go

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ type WorkspaceInfo struct {
7878
// (parsed from URL)
7979
IDEPublicPort string
8080

81-
Ports []PortInfo
82-
Auth *wsapi.WorkspaceAuthentication
81+
Ports []PortInfo
82+
Auth *wsapi.WorkspaceAuthentication
83+
Phase wsapi.WorkspacePhase
84+
StartedAt time.Time
8385
}
8486

8587
// PortInfo contains all information ws-proxy needs to know about a workspace port
@@ -265,7 +267,7 @@ func (p *RemoteWorkspaceInfoProvider) listen(client wsapi.WorkspaceManagerClient
265267
}
266268

267269
if status.Phase == wsapi.WorkspacePhase_STOPPED {
268-
p.cache.Delete(status.Metadata.MetaId)
270+
p.cache.Delete(status.Metadata.MetaId, status.Id)
269271
} else {
270272
info := mapWorkspaceStatusToInfo(status)
271273
p.cache.Insert(info)
@@ -309,10 +311,12 @@ func mapWorkspaceStatusToInfo(status *wsapi.WorkspaceStatus) *WorkspaceInfo {
309311
IDEPublicPort: getPortStr(status.Spec.Url),
310312
Ports: portInfos,
311313
Auth: status.Auth,
314+
Phase: status.Phase,
315+
StartedAt: status.Metadata.StartedAt.AsTime(),
312316
}
313317
}
314318

315-
// WorkspaceInfo return the WorkspaceInfo avaiable for the given workspaceID.
319+
// WorkspaceInfo return the WorkspaceInfo available for the given workspaceID.
316320
// Callers should make sure their context gets canceled properly. For good measure
317321
// this function will timeout by itself as well.
318322
func (p *RemoteWorkspaceInfoProvider) WorkspaceInfo(ctx context.Context, workspaceID string) *WorkspaceInfo {
@@ -355,10 +359,14 @@ func getPortStr(urlStr string) string {
355359
return portURL.Port()
356360
}
357361

362+
// TODO(rl): type def workspaceID and instanceID throughout for better readability and type safety?
363+
type workspaceInfosByInstance map[string]*WorkspaceInfo
364+
type instanceInfosByWorkspace map[string]workspaceInfosByInstance
365+
358366
// workspaceInfoCache stores WorkspaceInfo in a manner which is easy to query for WorkspaceInfoProvider
359367
type workspaceInfoCache struct {
360-
// WorkspaceInfos indexed by workspaceID
361-
infos map[string]*WorkspaceInfo
368+
// WorkspaceInfos indexed by workspaceID and then instanceID
369+
infos instanceInfosByWorkspace
362370
// WorkspaceCoords indexed by public (proxy) port (string)
363371
coordsByPublicPort map[string]*WorkspaceCoords
364372

@@ -371,7 +379,7 @@ type workspaceInfoCache struct {
371379
func newWorkspaceInfoCache() *workspaceInfoCache {
372380
var mu sync.RWMutex
373381
return &workspaceInfoCache{
374-
infos: make(map[string]*WorkspaceInfo),
382+
infos: make(map[string]workspaceInfosByInstance),
375383
coordsByPublicPort: make(map[string]*WorkspaceCoords),
376384
mu: &mu,
377385
cond: sync.NewCond(&mu),
@@ -382,7 +390,7 @@ func (c *workspaceInfoCache) Reinit(infos []*WorkspaceInfo) {
382390
c.cond.L.Lock()
383391
defer c.cond.L.Unlock()
384392

385-
c.infos = make(map[string]*WorkspaceInfo, len(infos))
393+
c.infos = make(map[string]workspaceInfosByInstance, len(infos))
386394
c.coordsByPublicPort = make(map[string]*WorkspaceCoords, len(c.coordsByPublicPort))
387395

388396
for _, info := range infos {
@@ -400,7 +408,15 @@ func (c *workspaceInfoCache) Insert(info *WorkspaceInfo) {
400408
}
401409

402410
func (c *workspaceInfoCache) doInsert(info *WorkspaceInfo) {
403-
c.infos[info.WorkspaceID] = info
411+
existingInfos, ok := c.infos[info.WorkspaceID]
412+
if !ok {
413+
existingInfos = make(map[string]*WorkspaceInfo)
414+
}
415+
existingInfos[info.InstanceID] = info
416+
c.infos[info.WorkspaceID] = existingInfos
417+
418+
// NOTE: in the unlikely event that a subsequent insert changes the port
419+
// then we add it here assuming that the delete will remove it
404420
c.coordsByPublicPort[info.IDEPublicPort] = &WorkspaceCoords{
405421
ID: info.WorkspaceID,
406422
}
@@ -413,25 +429,66 @@ func (c *workspaceInfoCache) doInsert(info *WorkspaceInfo) {
413429
}
414430
}
415431

416-
func (c *workspaceInfoCache) Delete(workspaceID string) {
432+
func (c *workspaceInfoCache) Delete(workspaceID string, instanceID string) {
417433
c.cond.L.Lock()
418434
defer c.cond.L.Unlock()
419435

420-
info, present := c.infos[workspaceID]
421-
if !present || info == nil {
436+
infos, present := c.infos[workspaceID]
437+
if !present || infos == nil {
422438
return
423439
}
424-
delete(c.coordsByPublicPort, info.IDEPublicPort)
425-
delete(c.infos, workspaceID)
440+
441+
// Keep only the active instances and public port(s) for the workspace id
442+
var instanceIDEPublicPort string
443+
if info, ok := infos[instanceID]; ok {
444+
delete(infos, instanceID)
445+
instanceIDEPublicPort = info.IDEPublicPort
446+
}
447+
448+
if len(infos) == 0 {
449+
delete(c.infos, workspaceID)
450+
} else {
451+
c.infos[workspaceID] = infos
452+
}
453+
454+
// Clean up the public port if port no longer active
455+
activePublicPorts := make(map[string]interface{})
456+
for _, info := range infos {
457+
activePublicPorts[info.IDEPublicPort] = true
458+
}
459+
460+
if _, ok := activePublicPorts[instanceIDEPublicPort]; !ok {
461+
log.WithField("workspaceID", workspaceID).WithField("instanceID", instanceID).WithField("port", instanceIDEPublicPort).Debug("deleting port for workspace")
462+
delete(c.coordsByPublicPort, instanceIDEPublicPort)
463+
}
426464
}
427465

428466
// WaitFor waits for workspace info until that info is available or the context is canceled.
429467
func (c *workspaceInfoCache) WaitFor(ctx context.Context, workspaceID string) (w *WorkspaceInfo, ok bool) {
430468
c.mu.RLock()
431-
w, ok = c.infos[workspaceID]
469+
existing, ok := c.infos[workspaceID]
432470
c.mu.RUnlock()
471+
472+
getCandidate := func(infos map[string]*WorkspaceInfo) *WorkspaceInfo {
473+
// Find the *best* candidate i.e. prefer any instance running over stopping/stopped
474+
// If there are >1 instances in running state it will pick the most recently started
475+
// as opposed to the random iteration order (which is harder to test)
476+
// NOTE: Yes, these is a potential issue with clock drift
477+
// but this scenario is extremely unlikely to happen in the wild
478+
var candidate *WorkspaceInfo
479+
for _, info := range infos {
480+
if candidate == nil ||
481+
(info.Phase == wsapi.WorkspacePhase_RUNNING &&
482+
(candidate.Phase != info.Phase || info.StartedAt.After(candidate.StartedAt))) ||
483+
(candidate.Phase > wsapi.WorkspacePhase_RUNNING && candidate.Phase < info.Phase) {
484+
candidate = info
485+
}
486+
}
487+
return candidate
488+
}
489+
433490
if ok {
434-
return
491+
return getCandidate(existing), true
435492
}
436493

437494
inc := make(chan *WorkspaceInfo)
@@ -446,12 +503,12 @@ func (c *workspaceInfoCache) WaitFor(ctx context.Context, workspaceID string) (w
446503
return
447504
}
448505

449-
info, ok := c.infos[workspaceID]
506+
infos, ok := c.infos[workspaceID]
450507
if !ok {
451508
continue
452509
}
453510

454-
inc <- info
511+
inc <- getCandidate(infos)
455512
return
456513
}
457514
}()

0 commit comments

Comments
 (0)