Search code examples
authenticationgowebsocketapi-design

How to "sync" a slice and map


I am making an API that will be used by another program. When the API authenticates a user it gives it an ID. The ID is then used to connect to a web-socket.

When the API initially sends the ID to the user, it adds it to a slice. This is to make sure that the user cannot have two sessions at the same time with only one set of credentials.

Once the user connects to the web-socket, the ID is sent and added to a map containing the connection & the ID.

When the connection closes, the ID is removed from the slice & the connection:ID pair is deleted from the map.

The problem is that if the authentication request completes, but the websocket connection doesn't, and the client program closes, the user will be locked out of the program as the ID will be in the slice. Is there a way to sync the slice with the mapping so that if the program cannot connect to the web-socket, they will not be locked out when they attempt to re-run the program?

EDIT:

I ended up switching to a single map and called the following function as my routes are setup:

    go func() {
        staleCheckInterval := time.Second * 10
        ticker := time.NewTicker(staleCheckInterval)
        for {
            select {
            case <-ticker.C:
                log.Println("checking stale ids")
                staleIds := []string{}
                for k, v := range handler.WsClients {
                    //if 10 seconds have passed and an Id does not have an associated connection, assume something went wrong.
                    if v.Ts < int(time.Now().UnixMilli())-10000 && v.WsConn == nil {
                        staleIds = append(staleIds, k)
                    }
                }
                for _, id := range staleIds {
                    delete(handler.WsClients, id)
                    log.Println("removed id:", id)
                }
            }
        }
    }()

Solution

  • You don't need "a way to sync a map and a slice"; you need a way to prevent the lockout condition you described.

    1. It doesn't sound like you need a separate map and a slice to begin with; you could do just fine with the map alone, with a field that contains the connection if they're connected, and is nil if the connection is still pending.

    2. When the authorization is made and the ID is issued, store the current timestamp into the map along with the ID.

    3. On authorization, if you find a record that's more than a few minutes old (or however long you decide is a reasonable timeout between authorization and connection) and doesn't have an active connection, delete it, act like it was never there, and create a new one. That way you don't have the "locked out" condition.

    4. Also have a task that runs periodically and removes stale entries from the map (this prevents it from accumulating millions of entries over time from users who authorized once, never connected, and never retried).