Skip to content

Commit b6312f0

Browse files
authored
make writes of the data from Write sequential with other writes (#55)
* make writes of the data from Write sequential with other writes * update the comment for Write method
1 parent 28e94e2 commit b6312f0

File tree

2 files changed

+105
-62
lines changed

2 files changed

+105
-62
lines changed

connection.go

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package connection
22

33
import (
44
"bufio"
5-
"bytes"
65
"crypto/tls"
76
"errors"
87
"fmt"
@@ -40,6 +39,12 @@ const (
4039
StatusUnknown Status = ""
4140
)
4241

42+
// directWrite is used to write data directly to the connection
43+
type directWrite struct {
44+
data []byte
45+
errCh chan error
46+
}
47+
4348
// Connection represents an ISO 8583 Connection. Connection may be used
4449
// by multiple goroutines simultaneously.
4550
type Connection struct {
@@ -48,6 +53,7 @@ type Connection struct {
4853
conn io.ReadWriteCloser
4954
requestsCh chan request
5055
readResponseCh chan *iso8583.Message
56+
directWriteCh chan directWrite
5157
done chan struct{}
5258

5359
// spec that will be used to unpack received messages
@@ -93,6 +99,7 @@ func New(addr string, spec *iso8583.MessageSpec, mlReader MessageLengthReader, m
9399
Opts: opts,
94100
requestsCh: make(chan request),
95101
readResponseCh: make(chan *iso8583.Message),
102+
directWriteCh: make(chan directWrite),
96103
done: make(chan struct{}),
97104
respMap: make(map[string]response),
98105
spec: spec,
@@ -169,13 +176,25 @@ func (c *Connection) Connect() error {
169176
return nil
170177
}
171178

172-
// Write writes data directly to the connection. Writes are atomic for
173-
// net.TCPConn and tls.Conn and can be called simultaneously from multiple
174-
// goroutines. But you should write whole message (including its header, etc.)
175-
// at once, don't split one message into multiple Write calls.
176-
// It's the caller's responsibility to handle the error returned from Write.
179+
// Write writes data directly to the connection. It is crucial to note that the
180+
// Write operation is atomic in nature, meaning it completes in a single
181+
// uninterrupted step.
182+
// When writing data, the entire message—including its header and any other
183+
// components—should be written in one go. Splitting a single message into
184+
// multiple Write calls is dangerous, as it could lead to unexpected behavior
185+
// or errors.
177186
func (c *Connection) Write(p []byte) (int, error) {
178-
return c.conn.Write(p)
187+
dw := directWrite{
188+
data: p,
189+
errCh: make(chan error, 1),
190+
}
191+
192+
select {
193+
case c.directWriteCh <- dw:
194+
return len(p), <-dw.errCh
195+
case <-c.done:
196+
return 0, ErrConnectionClosed
197+
}
179198
}
180199

181200
// run starts read and write loops in goroutines
@@ -380,34 +399,30 @@ func (c *Connection) Send(message *iso8583.Message) (*iso8583.Message, error) {
380399

381400
func (c *Connection) writeMessage(w io.Writer, message *iso8583.Message) error {
382401
if c.Opts.MessageWriter != nil {
383-
return c.Opts.MessageWriter.WriteMessage(w, message)
402+
err := c.Opts.MessageWriter.WriteMessage(c.conn, message)
403+
if err != nil {
404+
return fmt.Errorf("writing message: %w", err)
405+
}
406+
407+
return nil
384408
}
385409

386-
// default message writer
410+
// if no custom message writer is set, use default one
411+
387412
packed, err := message.Pack()
388413
if err != nil {
389414
return fmt.Errorf("packing message: %w", err)
390415
}
391416

392-
// create buffer for header and packed message so we can write it to
393-
// the connection as a single write
394-
buf := &bytes.Buffer{}
395-
396417
// create header
397-
_, err = c.writeMessageLength(buf, len(packed))
398-
if err != nil {
399-
return fmt.Errorf("writing message header to buffer: %w", err)
400-
}
401-
402-
_, err = buf.Write(packed)
418+
_, err = c.writeMessageLength(c.conn, len(packed))
403419
if err != nil {
404-
return fmt.Errorf("writing packed message to buffer: %w", err)
420+
return fmt.Errorf("writing message length: %w", err)
405421
}
406422

407-
// write buffer to the connection as a single write (atomic)
408-
_, err = buf.WriteTo(w)
423+
_, err = c.conn.Write(packed)
409424
if err != nil {
410-
return fmt.Errorf("writing buffer to the connection: %w", err)
425+
return fmt.Errorf("writing packed message: %w", err)
411426
}
412427

413428
return nil
@@ -533,6 +548,19 @@ func (c *Connection) writeLoop() {
533548
if req.replyCh == nil {
534549
req.errCh <- nil
535550
}
551+
552+
case dw := <-c.directWriteCh:
553+
_, err = c.conn.Write(dw.data)
554+
if err != nil {
555+
c.handleError(fmt.Errorf("writing data: %w", err))
556+
dw.errCh <- err
557+
558+
// we can't continue to write other messages or data when we failed to write
559+
// one of them
560+
break
561+
}
562+
dw.errCh <- nil
563+
536564
case <-idleTimeTimer.C:
537565
// if no message was sent during idle time, we have to send ping message
538566
if c.Opts.PingHandler != nil {

connection_test.go

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -178,58 +178,73 @@ func TestClient_Write(t *testing.T) {
178178
require.NoError(t, err)
179179
defer server.Close()
180180

181-
var called atomic.Int32
181+
t.Run("write into the connection", func(t *testing.T) {
182+
var called atomic.Int32
183+
inboundMessageHandler := func(c *connection.Connection, message *iso8583.Message) {
184+
called.Add(1)
182185

183-
inboundMessageHandler := func(c *connection.Connection, message *iso8583.Message) {
184-
called.Add(1)
186+
mti, err := message.GetMTI()
187+
require.NoError(t, err)
188+
require.Equal(t, "0810", mti)
189+
}
185190

186-
mti, err := message.GetMTI()
191+
// we should be able to write any bytes to the server
192+
c, err := connection.New(server.Addr, testSpec, readMessageLength, writeMessageLength, connection.InboundMessageHandler(inboundMessageHandler))
187193
require.NoError(t, err)
188-
require.Equal(t, "0810", mti)
189-
}
194+
defer c.Close()
190195

191-
// we should be able to write any bytes to the server
192-
c, err := connection.New(server.Addr, testSpec, readMessageLength, writeMessageLength, connection.InboundMessageHandler(inboundMessageHandler))
193-
require.NoError(t, err)
194-
defer c.Close()
196+
err = c.Connect()
197+
require.NoError(t, err)
195198

196-
err = c.Connect()
197-
require.NoError(t, err)
199+
// let's create data to write to the server, we will prepare header and
200+
// packed message
201+
202+
// network management message
203+
message := iso8583.NewMessage(testSpec)
204+
err = message.Marshal(baseFields{
205+
MTI: field.NewStringValue("0800"),
206+
TestCaseCode: field.NewStringValue(TestCaseReply),
207+
STAN: field.NewStringValue(getSTAN()),
208+
})
209+
require.NoError(t, err)
198210

199-
// let's create data to write to the server, we will prepare header and
200-
// packed message
211+
packed, err := message.Pack()
212+
require.NoError(t, err)
201213

202-
// network management message
203-
message := iso8583.NewMessage(testSpec)
204-
err = message.Marshal(baseFields{
205-
MTI: field.NewStringValue("0800"),
206-
TestCaseCode: field.NewStringValue(TestCaseReply),
207-
STAN: field.NewStringValue(getSTAN()),
208-
})
209-
require.NoError(t, err)
214+
// prepare header
215+
header := &bytes.Buffer{}
216+
_, err = writeMessageLength(header, len(packed))
217+
require.NoError(t, err)
210218

211-
packed, err := message.Pack()
212-
require.NoError(t, err)
219+
// combine header and message
220+
data := append(header.Bytes(), packed...)
213221

214-
// prepare header
215-
header := &bytes.Buffer{}
216-
_, err = writeMessageLength(header, len(packed))
217-
require.NoError(t, err)
222+
// write the data directly to the connection
223+
n, err := c.Write(data)
224+
225+
require.NoError(t, err)
226+
require.Equal(t, len(data), n)
227+
228+
// we should expect to get reply, but as we are not using Send method,
229+
// the reply will be handled by InboundMessageHandler
230+
require.Eventually(t, func() bool {
231+
return called.Load() == 1
232+
}, 100*time.Millisecond, 20*time.Millisecond, "inboundMessageHandler should be called")
233+
})
218234

219-
// combine header and message
220-
data := append(header.Bytes(), packed...)
235+
t.Run("write into the closed connection ", func(t *testing.T) {
236+
c, err := connection.New(server.Addr, testSpec, readMessageLength, writeMessageLength)
237+
require.NoError(t, err)
238+
defer c.Close()
221239

222-
// write the data directly to the connection
223-
n, err := c.Write(data)
240+
err = c.Connect()
241+
require.NoError(t, err)
224242

225-
require.NoError(t, err)
226-
require.Equal(t, len(data), n)
243+
c.Close()
227244

228-
// we should expect to get reply, but as we are not using Send method,
229-
// the reply will be handled by InboundMessageHandler
230-
require.Eventually(t, func() bool {
231-
return called.Load() == 1
232-
}, 100*time.Millisecond, 20*time.Millisecond, "inboundMessageHandler should be called")
245+
_, err = c.Write([]byte("hello"))
246+
require.Error(t, err)
247+
})
233248
}
234249

235250
func TestClient_Send(t *testing.T) {

0 commit comments

Comments
 (0)