Skip to content

Commit 2e3c226

Browse files
committed
Fix for service import cycles
Signed-off-by: Derek Collison <[email protected]>
1 parent a0c4c5c commit 2e3c226

File tree

4 files changed

+84
-46
lines changed

4 files changed

+84
-46
lines changed

server/accounts.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,10 +1343,26 @@ func (a *Account) AddServiceImportWithClaim(destination *Account, from, to strin
13431343
return ErrServiceImportAuthorization
13441344
}
13451345

1346+
if a.importFormsCycle(destination, from, to) {
1347+
return ErrServiceImportFormsCycle
1348+
}
1349+
13461350
_, err := a.addServiceImport(destination, from, to, imClaim)
13471351
return err
13481352
}
13491353

1354+
// Detects if we have a cycle.
1355+
func (a *Account) importFormsCycle(destination *Account, from, to string) bool {
1356+
// Check that what we are importing is not something we also export.
1357+
if a.serviceExportOverlaps(to) {
1358+
// So at this point if destination account is also importing from us, that forms a cycle.
1359+
if destination.serviceImportOverlaps(from) {
1360+
return true
1361+
}
1362+
}
1363+
return false
1364+
}
1365+
13501366
// SetServiceImportSharing will allow sharing of information about requests with the export account.
13511367
// Used for service latency tracking at the moment.
13521368
func (a *Account) SetServiceImportSharing(destination *Account, to string, allow bool) error {
@@ -1585,6 +1601,30 @@ func (a *Account) checkForReverseEntry(reply string, si *serviceImport, checkInt
15851601
}
15861602
}
15871603

1604+
// Internal check to see if the to subject overlaps with another export.
1605+
func (a *Account) serviceExportOverlaps(to string) bool {
1606+
a.mu.RLock()
1607+
defer a.mu.RUnlock()
1608+
for subj := range a.exports.services {
1609+
if to == subj || SubjectsCollide(to, subj) {
1610+
return true
1611+
}
1612+
}
1613+
return false
1614+
}
1615+
1616+
// Internal check to see if the from subject overlaps with another import.
1617+
func (a *Account) serviceImportOverlaps(from string) bool {
1618+
a.mu.RLock()
1619+
defer a.mu.RUnlock()
1620+
for subj := range a.imports.services {
1621+
if from == subj || SubjectsCollide(from, subj) {
1622+
return true
1623+
}
1624+
}
1625+
return false
1626+
}
1627+
15881628
// Internal check to see if a service import exists.
15891629
func (a *Account) serviceImportExists(dest *Account, from string) bool {
15901630
a.mu.RLock()

server/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3705,6 +3705,7 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
37053705
}
37063706
continue
37073707
}
3708+
37083709
// Assume delivery subject is the normal subject to this point.
37093710
dsubj = subj
37103711

server/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ var (
125125
// ErrServiceImportAuthorization is returned when a service import is not authorized.
126126
ErrServiceImportAuthorization = errors.New("service import not authorized")
127127

128+
// ErrServiceImportFormsCycle is returned when a service import forms a cycle.
129+
ErrServiceImportFormsCycle = errors.New("service import forms cycle")
130+
128131
// ErrClientOrRouteConnectedToGatewayPort represents an error condition when
129132
// a client or route attempted to connect to the Gateway port.
130133
ErrClientOrRouteConnectedToGatewayPort = errors.New("attempted to connect to gateway port")

test/service_latency_test.go

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,68 +1304,62 @@ func TestServiceAndStreamStackOverflow(t *testing.T) {
13041304
}
13051305

13061306
func TestServiceCycle(t *testing.T) {
1307-
t.Skip("Remove the skip to run this test and shows the issue")
1308-
13091307
conf := createConfFile(t, []byte(`
13101308
accounts {
13111309
A {
1312-
users = [ { user: "a", pass: "a" } ]
1313-
exports [
1314-
{ service: help }
1315-
]
1316-
imports [
1317-
{ service { subject: help, account: B } }
1318-
]
1310+
exports [ { service: help } ]
1311+
imports [ { service { subject: help, account: B } } ]
13191312
}
13201313
B {
1321-
users = [ { user: "b", pass: "b" } ]
1322-
exports [
1323-
{ service: help }
1324-
]
1325-
imports [
1326-
{ service { subject: help, account: A } }
1327-
]
1314+
exports [ { service: help } ]
1315+
imports [ { service { subject: help, account: A } } ]
13281316
}
13291317
}
13301318
`))
13311319
defer os.Remove(conf)
13321320

1333-
srv, opts := RunServerWithConfig(conf)
1334-
defer srv.Shutdown()
1335-
1336-
// Responder (just request sub)
1337-
nc, err := nats.Connect(fmt.Sprintf("nats://a:a@%s:%d", opts.Host, opts.Port))
1338-
if err != nil {
1339-
t.Fatalf("Error on connect: %v", err)
1321+
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrServiceImportFormsCycle.Error()) {
1322+
t.Fatalf("Expected an error on cycle service import, got none")
13401323
}
1341-
defer nc.Close()
13421324

1343-
cb := func(m *nats.Msg) {
1344-
m.Respond(nil)
1345-
}
1346-
sub, _ := nc.Subscribe("help", cb)
1347-
nc.Flush()
1348-
1349-
// Requestor
1350-
nc2, err := nats.Connect(fmt.Sprintf("nats://b:b@%s:%d", opts.Host, opts.Port))
1351-
if err != nil {
1352-
t.Fatalf("Error on connect: %v", err)
1353-
}
1354-
defer nc2.Close()
1325+
conf = createConfFile(t, []byte(`
1326+
accounts {
1327+
A {
1328+
exports [ { service: * } ]
1329+
imports [ { service { subject: help, account: B } } ]
1330+
}
1331+
B {
1332+
exports [ { service: help } ]
1333+
imports [ { service { subject: *, account: A } } ]
1334+
}
1335+
}
1336+
`))
1337+
defer os.Remove(conf)
13551338

1356-
// Send a single request.
1357-
if _, err := nc2.Request("help", []byte("hi"), time.Second); err != nil {
1358-
t.Fatal("Did not get the reply")
1339+
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrServiceImportFormsCycle.Error()) {
1340+
t.Fatalf("Expected an error on cycle service import, got none")
13591341
}
13601342

1361-
// Make sure works for queue subscribers as well.
1362-
sub.Unsubscribe()
1363-
sub, _ = nc.QueueSubscribe("help", "prod", cb)
1364-
nc.Flush()
1343+
conf = createConfFile(t, []byte(`
1344+
accounts {
1345+
A {
1346+
exports [ { service: * } ]
1347+
imports [ { service { subject: help, account: B } } ]
1348+
}
1349+
B {
1350+
exports [ { service: help } ]
1351+
imports [ { service { subject: help, account: C } } ]
1352+
}
1353+
C {
1354+
exports [ { service: * } ]
1355+
imports [ { service { subject: *, account: A } } ]
1356+
}
1357+
}
1358+
`))
1359+
defer os.Remove(conf)
13651360

1366-
// Send a single request.
1367-
if _, err := nc2.Request("help", []byte("hi"), time.Second); err != nil {
1368-
t.Fatal("Did not get the reply")
1361+
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrServiceImportFormsCycle.Error()) {
1362+
t.Fatalf("Expected an error on cycle service import, got none")
13691363
}
13701364
}
13711365

0 commit comments

Comments
 (0)