Skip to content

Commit 632f860

Browse files
Add solution for Challenge 8 by Kosench (#749)
* Add solution for Challenge 8 * Fix race conditions and sync issues * fix: resolve race conditions in client send and map access --------- Co-authored-by: go-interview-practice-bot[bot] <230190823+go-interview-practice-bot[bot]@users.noreply.github.com>
1 parent 4d41442 commit 632f860

File tree

1 file changed

+313
-0
lines changed

1 file changed

+313
-0
lines changed
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
package challenge8
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"sync"
7+
)
8+
9+
// Message represents a message to be delivered
10+
type Message struct {
11+
Sender *Client
12+
Content string
13+
Recipient string // empty for broadcast
14+
}
15+
16+
// joinRequest represents a request to join the chat
17+
type joinRequest struct {
18+
username string
19+
response chan *Client
20+
errChan chan error
21+
}
22+
23+
// leaveRequest represents a request to leave the chat
24+
type leaveRequest struct {
25+
client *Client
26+
done chan struct{}
27+
}
28+
29+
// Client represents a connected chat client
30+
type Client struct {
31+
username string
32+
messages chan string
33+
server *ChatServer
34+
mu sync.RWMutex
35+
active bool
36+
}
37+
38+
func newClient(username string, server *ChatServer) *Client {
39+
return &Client{
40+
username: username,
41+
messages: make(chan string, 50),
42+
server: server,
43+
active: true,
44+
}
45+
}
46+
47+
// Send sends a message to the client (non-blocking)
48+
func (c *Client) Send(message string) {
49+
c.mu.RLock()
50+
defer c.mu.RUnlock()
51+
52+
if !c.active {
53+
return
54+
}
55+
56+
// Non-blocking send (protected by RLock to prevent close during send)
57+
select {
58+
case c.messages <- message:
59+
default:
60+
// Channel full, drop message
61+
}
62+
}
63+
64+
// Receive returns the next message for the client (blocking)
65+
func (c *Client) Receive() string {
66+
c.mu.RLock()
67+
msgChan := c.messages
68+
c.mu.RUnlock()
69+
70+
msg, ok := <-msgChan
71+
if !ok {
72+
return ""
73+
}
74+
return msg
75+
}
76+
77+
// isActive checks if client is still active
78+
func (c *Client) isActive() bool {
79+
c.mu.RLock()
80+
defer c.mu.RUnlock()
81+
return c.active
82+
}
83+
84+
// markInactive marks client as inactive and closes channel
85+
func (c *Client) markInactive() {
86+
c.mu.Lock()
87+
defer c.mu.Unlock()
88+
89+
if c.active {
90+
c.active = false
91+
close(c.messages)
92+
}
93+
}
94+
95+
// ChatServer manages client connections and message routing
96+
type ChatServer struct {
97+
clients map[string]*Client
98+
mu sync.RWMutex // Protects clients map (read and write)
99+
broadcast chan Message
100+
join chan joinRequest
101+
leave chan leaveRequest
102+
shutdown chan struct{}
103+
wg sync.WaitGroup
104+
}
105+
106+
// NewChatServer creates a new chat server instance
107+
func NewChatServer() *ChatServer {
108+
server := &ChatServer{
109+
clients: make(map[string]*Client),
110+
broadcast: make(chan Message, 100),
111+
join: make(chan joinRequest),
112+
leave: make(chan leaveRequest),
113+
shutdown: make(chan struct{}),
114+
}
115+
116+
server.wg.Add(1)
117+
go server.run()
118+
119+
return server
120+
}
121+
122+
// run is the central goroutine that handles all state modifications
123+
func (s *ChatServer) run() {
124+
defer s.wg.Done()
125+
126+
for {
127+
select {
128+
case req := <-s.join:
129+
// Check for duplicate username and register client under lock
130+
s.mu.Lock()
131+
if _, exists := s.clients[req.username]; exists {
132+
s.mu.Unlock()
133+
req.errChan <- ErrUsernameAlreadyTaken
134+
close(req.response)
135+
close(req.errChan)
136+
continue
137+
}
138+
139+
client := newClient(req.username, s)
140+
s.clients[req.username] = client
141+
s.mu.Unlock()
142+
143+
// Send response
144+
req.response <- client
145+
req.errChan <- nil
146+
close(req.response)
147+
close(req.errChan)
148+
149+
case req := <-s.leave:
150+
// Remove client if exists
151+
s.mu.Lock()
152+
client, exists := s.clients[req.client.username]
153+
if exists {
154+
delete(s.clients, req.client.username)
155+
}
156+
s.mu.Unlock()
157+
158+
if exists {
159+
client.markInactive()
160+
}
161+
close(req.done)
162+
163+
case msg := <-s.broadcast:
164+
// Deliver message (read clients map under lock)
165+
s.mu.RLock()
166+
if msg.Recipient == "" {
167+
// Broadcast to all except sender
168+
for _, client := range s.clients {
169+
if client != msg.Sender && client.isActive() {
170+
client.Send(msg.Content)
171+
}
172+
}
173+
} else {
174+
// Private message
175+
if recipient, exists := s.clients[msg.Recipient]; exists && recipient.isActive() {
176+
recipient.Send(msg.Content)
177+
}
178+
}
179+
s.mu.RUnlock()
180+
181+
case <-s.shutdown:
182+
// Cleanup all clients
183+
s.mu.RLock()
184+
clientsCopy := make([]*Client, 0, len(s.clients))
185+
for _, client := range s.clients {
186+
clientsCopy = append(clientsCopy, client)
187+
}
188+
s.mu.RUnlock()
189+
190+
// Mark inactive outside the lock to avoid deadlock
191+
for _, client := range clientsCopy {
192+
client.markInactive()
193+
}
194+
195+
s.mu.Lock()
196+
s.clients = make(map[string]*Client)
197+
s.mu.Unlock()
198+
return
199+
}
200+
}
201+
}
202+
203+
// Connect adds a new client to the chat server
204+
func (s *ChatServer) Connect(username string) (*Client, error) {
205+
if username == "" {
206+
return nil, ErrEmptyUsername
207+
}
208+
209+
// Create request channels
210+
req := joinRequest{
211+
username: username,
212+
response: make(chan *Client, 1),
213+
errChan: make(chan error, 1),
214+
}
215+
216+
// Send join request to central goroutine
217+
s.join <- req
218+
219+
// Wait for response (blocking until processed)
220+
client := <-req.response
221+
err := <-req.errChan
222+
223+
if err != nil {
224+
return nil, err
225+
}
226+
227+
return client, nil
228+
}
229+
230+
// Disconnect removes a client from the chat server
231+
func (s *ChatServer) Disconnect(client *Client) {
232+
if client == nil {
233+
return
234+
}
235+
236+
// Create request with done channel
237+
req := leaveRequest{
238+
client: client,
239+
done: make(chan struct{}),
240+
}
241+
242+
// Send leave request
243+
s.leave <- req
244+
245+
// Wait for completion (blocking until processed)
246+
<-req.done
247+
}
248+
249+
// Broadcast sends a message to all connected clients except sender
250+
func (s *ChatServer) Broadcast(sender *Client, message string) {
251+
if sender == nil || !sender.isActive() {
252+
return
253+
}
254+
255+
formattedMsg := fmt.Sprintf("[%s]: %s", sender.username, message)
256+
257+
msg := Message{
258+
Sender: sender,
259+
Content: formattedMsg,
260+
Recipient: "",
261+
}
262+
263+
select {
264+
case s.broadcast <- msg:
265+
default:
266+
// Broadcast channel full, skip
267+
}
268+
}
269+
270+
// PrivateMessage sends a message to a specific client
271+
func (s *ChatServer) PrivateMessage(sender *Client, recipientUsername string, message string) error {
272+
if sender == nil || !sender.isActive() {
273+
return ErrClientDisconnected
274+
}
275+
276+
// Check if recipient exists (under read lock)
277+
s.mu.RLock()
278+
_, exists := s.clients[recipientUsername]
279+
s.mu.RUnlock()
280+
281+
if !exists {
282+
return ErrRecipientNotFound
283+
}
284+
285+
formattedMsg := fmt.Sprintf("[PM from %s]: %s", sender.username, message)
286+
287+
msg := Message{
288+
Sender: sender,
289+
Content: formattedMsg,
290+
Recipient: recipientUsername,
291+
}
292+
293+
select {
294+
case s.broadcast <- msg:
295+
return nil
296+
default:
297+
return errors.New("message queue full")
298+
}
299+
}
300+
301+
// Shutdown gracefully shuts down the chat server
302+
func (s *ChatServer) Shutdown() {
303+
close(s.shutdown)
304+
s.wg.Wait()
305+
}
306+
307+
// Common errors that can be returned by the Chat Server
308+
var (
309+
ErrUsernameAlreadyTaken = errors.New("username already taken")
310+
ErrRecipientNotFound = errors.New("recipient not found")
311+
ErrClientDisconnected = errors.New("client disconnected")
312+
ErrEmptyUsername = errors.New("username cannot be empty")
313+
)

0 commit comments

Comments
 (0)