Skip to content

Commit 4266028

Browse files
authored
Merge pull request #342 from rdkcentral/cherrypick-cassandra-timeout-504
Cherrypick cassandra timeout 504
2 parents dff8f66 + be83191 commit 4266028

11 files changed

Lines changed: 325 additions & 38 deletions

db/cassandra/cassandra_client.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package cassandra
1919

2020
import (
21+
"context"
2122
"crypto/tls"
2223
"crypto/x509"
2324
"errors"
@@ -343,6 +344,32 @@ func (c *CassandraClient) IsDbNotFound(err error) bool {
343344
return errors.Is(err, gocql.ErrNotFound)
344345
}
345346

347+
func (c *CassandraClient) IsDbTimeout(err error) bool {
348+
if err == nil {
349+
return false
350+
}
351+
if errors.Is(err, gocql.ErrTimeoutNoResponse) {
352+
return true
353+
}
354+
if errors.Is(err, gocql.ErrConnectionClosed) {
355+
return true
356+
}
357+
if errors.Is(err, context.DeadlineExceeded) {
358+
return true
359+
}
360+
// context.Canceled is excluded: it indicates caller cancellation (client disconnect),
361+
// not a DB/network timeout. Mapping it to 504 would misrepresent the failure cause.
362+
var readTimeout *gocql.RequestErrReadTimeout
363+
if errors.As(err, &readTimeout) {
364+
return true
365+
}
366+
var writeTimeout *gocql.RequestErrWriteTimeout
367+
if errors.As(err, &writeTimeout) {
368+
return true
369+
}
370+
return false
371+
}
372+
346373
func (c *CassandraClient) Close() error {
347374
c.Session.Close()
348375
return nil

db/database_client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ type DatabaseClient interface {
4848
// not found
4949
IsDbNotFound(error) bool
5050

51+
// timeout
52+
IsDbTimeout(error) bool
53+
5154
// set metrics
5255
Metrics() *common.AppMetrics
5356
SetMetrics(*common.AppMetrics)

db/sqlite/sqlite_client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ func (c *SqliteClient) IsDbNotFound(err error) bool {
130130
return false
131131
}
132132

133+
func (c *SqliteClient) IsDbTimeout(err error) bool {
134+
return false
135+
}
136+
133137
func (c *SqliteClient) Metrics() *common.AppMetrics {
134138
return c.AppMetrics
135139
}

http/cassandra_timeout_test.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/**
2+
* Copyright 2021 Comcast Cable Communications Management, LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
package http
19+
20+
import (
21+
"bytes"
22+
"context"
23+
"errors"
24+
"fmt"
25+
"net/http"
26+
"testing"
27+
28+
"github.com/gocql/gocql"
29+
"github.com/prometheus/client_golang/prometheus"
30+
"github.com/rdkcentral/webconfig/common"
31+
"github.com/rdkcentral/webconfig/db"
32+
"github.com/rdkcentral/webconfig/db/cassandra"
33+
"github.com/rdkcentral/webconfig/util"
34+
"gotest.tools/assert"
35+
)
36+
37+
// errSimulatedTimeout is a sentinel used by timeoutMockClient to trigger the 504 path.
38+
var errSimulatedTimeout = errors.New("simulated cassandra timeout")
39+
40+
// timeoutMockClient wraps the real (SQLite) DatabaseClient and overrides IsDbTimeout plus
41+
// selected read methods to return errSimulatedTimeout, simulating a Cassandra timeout
42+
// without a live Cassandra instance. Overridden methods:
43+
// - IsDbTimeout — recognises errSimulatedTimeout as a timeout
44+
// - GetSubDocument — used by GetSubDocumentHandler (GET /document/{id})
45+
// - GetRootDocumentLabels — used by PostSubDocumentHandler (POST /document/{id})
46+
// - GetRootDocument — used by BuildGetDocument (GET /config)
47+
// - GetDocument — used by BuildGetDocument fallback paths
48+
//
49+
// All other interface methods (SetSubDocument, DeleteDocument, etc.) fall through to the
50+
// embedded SQLite client and execute normally.
51+
type timeoutMockClient struct {
52+
db.DatabaseClient
53+
}
54+
55+
func (m *timeoutMockClient) IsDbTimeout(err error) bool {
56+
return errors.Is(err, errSimulatedTimeout)
57+
}
58+
59+
func (m *timeoutMockClient) GetSubDocument(mac, subdocId string) (*common.SubDocument, error) {
60+
return nil, errSimulatedTimeout
61+
}
62+
63+
func (m *timeoutMockClient) GetRootDocumentLabels(mac string) (prometheus.Labels, error) {
64+
return nil, errSimulatedTimeout
65+
}
66+
67+
func (m *timeoutMockClient) GetRootDocument(mac string) (*common.RootDocument, error) {
68+
return nil, errSimulatedTimeout
69+
}
70+
71+
func (m *timeoutMockClient) GetDocument(mac string, args ...interface{}) (*common.Document, error) {
72+
return nil, errSimulatedTimeout
73+
}
74+
75+
// TestIsDbTimeout tests CassandraClient.IsDbTimeout against all gocql timeout error
76+
// variants — including connection-closed and context deadline — as well as direct errors,
77+
// wrapped errors, and multi-layer chains. Non-timeout errors must return false.
78+
// No live Cassandra connection is required because IsDbTimeout is a pure error-inspection
79+
// function.
80+
func TestIsDbTimeout(t *testing.T) {
81+
c := &cassandra.CassandraClient{}
82+
83+
cases := []struct {
84+
name string
85+
err error
86+
want bool
87+
}{
88+
{
89+
name: "ErrTimeoutNoResponse direct",
90+
err: gocql.ErrTimeoutNoResponse,
91+
want: true,
92+
},
93+
{
94+
name: "ErrTimeoutNoResponse wrapped once",
95+
err: fmt.Errorf("layer: %w", gocql.ErrTimeoutNoResponse),
96+
want: true,
97+
},
98+
{
99+
name: "ErrTimeoutNoResponse wrapped via common.NewError",
100+
err: common.NewError(gocql.ErrTimeoutNoResponse),
101+
want: true,
102+
},
103+
{
104+
name: "ErrTimeoutNoResponse wrapped multiple times",
105+
err: fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", gocql.ErrTimeoutNoResponse)),
106+
want: true,
107+
},
108+
{
109+
name: "RequestErrReadTimeout direct",
110+
err: &gocql.RequestErrReadTimeout{},
111+
want: true,
112+
},
113+
{
114+
name: "RequestErrReadTimeout wrapped",
115+
err: fmt.Errorf("layer: %w", &gocql.RequestErrReadTimeout{}),
116+
want: true,
117+
},
118+
{
119+
name: "RequestErrWriteTimeout direct",
120+
err: &gocql.RequestErrWriteTimeout{},
121+
want: true,
122+
},
123+
{
124+
name: "RequestErrWriteTimeout wrapped",
125+
err: fmt.Errorf("layer: %w", &gocql.RequestErrWriteTimeout{}),
126+
want: true,
127+
},
128+
{
129+
name: "ErrConnectionClosed direct",
130+
err: gocql.ErrConnectionClosed,
131+
want: true,
132+
},
133+
{
134+
name: "ErrConnectionClosed wrapped",
135+
err: fmt.Errorf("layer: %w", gocql.ErrConnectionClosed),
136+
want: true,
137+
},
138+
{
139+
name: "context.DeadlineExceeded direct",
140+
err: context.DeadlineExceeded,
141+
want: true,
142+
},
143+
{
144+
name: "context.DeadlineExceeded wrapped",
145+
err: fmt.Errorf("layer: %w", context.DeadlineExceeded),
146+
want: true,
147+
},
148+
{
149+
name: "context.Canceled direct",
150+
err: context.Canceled,
151+
want: false,
152+
},
153+
{
154+
name: "context.Canceled wrapped",
155+
err: fmt.Errorf("layer: %w", context.Canceled),
156+
want: false,
157+
},
158+
{
159+
name: "ErrNotFound is not a timeout",
160+
err: gocql.ErrNotFound,
161+
want: false,
162+
},
163+
{
164+
name: "generic error is not a timeout",
165+
err: errors.New("some db error"),
166+
want: false,
167+
},
168+
{
169+
name: "nil is not a timeout",
170+
err: nil,
171+
want: false,
172+
},
173+
}
174+
175+
for _, tc := range cases {
176+
t.Run(tc.name, func(t *testing.T) {
177+
got := c.IsDbTimeout(tc.err)
178+
assert.Equal(t, got, tc.want)
179+
})
180+
}
181+
}
182+
183+
// TestDbErrToStatus verifies that dbErrToStatus maps a timeout error to 504 and a
184+
// non-timeout error to 500, using the mock client to drive IsDbTimeout.
185+
func TestDbErrToStatus(t *testing.T) {
186+
server := NewWebconfigServer(sc, true)
187+
server.DatabaseClient = &timeoutMockClient{DatabaseClient: server.DatabaseClient}
188+
189+
assert.Equal(t, server.dbErrToStatus(errSimulatedTimeout), http.StatusGatewayTimeout)
190+
assert.Equal(t, server.dbErrToStatus(errors.New("other error")), http.StatusInternalServerError)
191+
}
192+
193+
// TestGetSubDocumentHandlerCassandraTimeout verifies that GET /document/{id} returns 504
194+
// when the database layer reports a Cassandra timeout on GetSubDocument.
195+
func TestGetSubDocumentHandlerCassandraTimeout(t *testing.T) {
196+
server := NewWebconfigServer(sc, true)
197+
server.DatabaseClient = &timeoutMockClient{DatabaseClient: server.DatabaseClient}
198+
router := server.GetRouter(true)
199+
200+
cpeMac := util.GenerateRandomCpeMac()
201+
url := fmt.Sprintf("/api/v1/device/%v/document/lan", cpeMac)
202+
req, err := http.NewRequest("GET", url, nil)
203+
assert.NilError(t, err)
204+
205+
res := ExecuteRequest(req, router).Result()
206+
assert.Equal(t, res.StatusCode, http.StatusGatewayTimeout)
207+
}
208+
209+
// TestPostSubDocumentHandlerCassandraTimeout verifies that POST /document/{id} returns 504
210+
// when the database layer reports a Cassandra timeout on GetRootDocumentLabels.
211+
func TestPostSubDocumentHandlerCassandraTimeout(t *testing.T) {
212+
server := NewWebconfigServer(sc, true)
213+
server.DatabaseClient = &timeoutMockClient{DatabaseClient: server.DatabaseClient}
214+
router := server.GetRouter(true)
215+
216+
cpeMac := util.GenerateRandomCpeMac()
217+
url := fmt.Sprintf("/api/v1/device/%v/document/lan", cpeMac)
218+
req, err := http.NewRequest("POST", url, bytes.NewReader([]byte{0x80}))
219+
assert.NilError(t, err)
220+
req.Header.Set(common.HeaderContentType, common.HeaderApplicationMsgpack)
221+
222+
res := ExecuteRequest(req, router).Result()
223+
assert.Equal(t, res.StatusCode, http.StatusGatewayTimeout)
224+
}
225+
226+
// TestMultipartConfigHandlerCassandraTimeout verifies that GET /config returns 504 when
227+
// the database layer reports a Cassandra timeout during document retrieval.
228+
func TestMultipartConfigHandlerCassandraTimeout(t *testing.T) {
229+
server := NewWebconfigServer(sc, true)
230+
server.DatabaseClient = &timeoutMockClient{DatabaseClient: server.DatabaseClient}
231+
router := server.GetRouter(true)
232+
233+
cpeMac := util.GenerateRandomCpeMac()
234+
url := fmt.Sprintf("/api/v1/device/%v/config", cpeMac)
235+
req, err := http.NewRequest("GET", url, nil)
236+
assert.NilError(t, err)
237+
req.Header.Set(common.HeaderSchemaVersion, "none")
238+
239+
res := ExecuteRequest(req, router).Result()
240+
assert.Equal(t, res.StatusCode, http.StatusGatewayTimeout)
241+
}

0 commit comments

Comments
 (0)