Go and concurrency noob here. I wrote a program to handle a tcp connection to index (and removes) packages. I want it to be able hundreds of connections and messages that might come in at the same time. I am using a hash table to store the data and using channels to provide the worker with the incoming messages.
My understanding is that I will not have to use any locks against the data store as all add/remove will be blocked until the previous connection has finished writing and reading. Just wanted to make sure I understand go channels correctly here and I wrote an efficient solution. In the future in lieu of the hash table does it make sense to use something like Redis? Or even store a list on the file system?
I would like to understand
- My use of channels is correct and additional locks won't be necessary. In the future the comments will be a list of other package dependencies so it's important each request can write to the data correctly
- I'm using a hash table as the data store but since this is in memory eventually would it make sense to use something like Redis? Or even writing to a local file
- How would one test sending hundreds of messages to this server?
- I do check for the format of the message but what happens if a client disconnects abruptly?
Usage of the app:
go run main.go
# in a separate terminal
echo -n "INDEX|vim|comment\n" | nc localhost 3333
echo -n "REMOVE|vim|blah,blah\n" | nc localhost 3333
echo -n "INDEX|vim|comment\n" | nc localhost 3333
echo -n "INDEX|vim|comment\n" | nc localhost 3333
echo -n "COUNT|vim|comment\n" | nc localhost 3333
The code
package main
import (
"bufio"
"errors"
"fmt"
"log"
"net"
"os"
"strings"
)
const (
CONN_HOST = "localhost"
CONN_PORT = "3333"
CONN_TYPE = "tcp"
INDEX_COMMAND = "INDEX"
REMOVE_COMMAND = "REMOVE"
COUNT_COMMAND = "COUNT"
)
func main() {
l, err := net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT)
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
defer l.Close()
fmt.Println("Listening on " + CONN_HOST + ":" + CONN_PORT)
msgCh := make(chan string)
resultCh := make(chan string)
ds := newDataStore()
go ds.msgHandlerWorker(msgCh, resultCh)
for {
conn, err := l.Accept()
if err != nil {
fmt.Println("Error accepting: ", err.Error())
os.Exit(1)
}
go handleRequest(conn, msgCh, resultCh)
}
}
type Message struct {
command string
pkg string
comment string
}
type DataStore struct {
pkgInfo map[string]string
pkgRef map[string]uint
}
func newDataStore() *DataStore {
return &DataStore{
pkgInfo: make(map[string]string),
pkgRef: make(map[string]uint),
}
}
func (ds *DataStore) parseMsg(msg string) (Message, error) {
var parsedMsg Message
m := strings.TrimSpace(msg)
s := strings.Split(m, "|")
if len(s) < 3 {
return parsedMsg, errors.New("Not correct format")
}
parsedMsg.command = s[0]
parsedMsg.pkg = s[1]
parsedMsg.comment = s[2]
return parsedMsg, nil
}
func (ds *DataStore) addToHashTable(msg Message) error {
ds.pkgInfo[msg.pkg] = msg.comment
if _, ok := ds.pkgRef[msg.pkg]; ok {
ds.pkgRef[msg.pkg] += 1
} else {
ds.pkgRef[msg.pkg] = 0
}
return nil
}
func (ds *DataStore) removeFromHashTable(msg Message) error {
delete(ds.pkgInfo, msg.pkg)
if _, ok := ds.pkgRef[msg.pkg]; ok {
ds.pkgRef[msg.pkg] -= 1
}
return nil
}
func (ds *DataStore) msgHandlerWorker(msgCh chan string, resultCh chan string) {
for {
msg := <-msgCh
parsedMsg, err := ds.parseMsg(msg)
if err != nil {
log.Print("pailed to parse msg")
resultCh <- "FAIL\n"
continue
}
switch parsedMsg.command {
case INDEX_COMMAND:
if err := ds.addToHashTable(parsedMsg); err != nil {
log.Print("Failed To add")
resultCh <- "FAIL\n"
continue
}
resultCh <- "OK\n"
case REMOVE_COMMAND:
if err := ds.removeFromHashTable(parsedMsg); err != nil {
log.Print("Failed To add")
resultCh <- "FAIL\n"
continue
}
resultCh <- "OK\n"
case COUNT_COMMAND:
if val, ok := ds.pkgRef[parsedMsg.pkg]; ok {
resultCh <- fmt.Sprintf("%d\n", val)
continue
}
resultCh <- "FAIL\n"
default:
log.Print("got invalid command")
resultCh <- "FAIL\n"
continue
}
}
}
func handleRequest(conn net.Conn, msgCh chan string, resultCh chan string) {
msg, err := bufio.NewReader(conn).ReadString('\n')
log.Printf("msg %v", msg)
if err != nil {
fmt.Println("Error reading:", err.Error())
}
msgCh <- msg
response := <-resultCh
conn.Write([]byte(response))
conn.Close()
}
Thanks for reading!