-
Notifications
You must be signed in to change notification settings - Fork 86
WIP: PostgreSQL LISTEN/NOTIFY: invalidate wit cache #2172
base: master
Are you sure you want to change the base?
Changes from 5 commits
35ad6db
04d3c8c
0bd16a6
1bfe26e
6857c09
5caadbd
b227b9f
68f445a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package gormsupport | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/fabric8-services/fabric8-wit/configuration" | ||
"github.com/fabric8-services/fabric8-wit/log" | ||
"github.com/lib/pq" | ||
errs "github.com/pkg/errors" | ||
) | ||
|
||
const ( | ||
// ChanSpaceTemplateUpdates is the name for the postgres notification | ||
// channel on which subscribers are informed about updates to the space | ||
// templates (e.g. when a migration has happened). | ||
ChanSpaceTemplateUpdates = "f8_space_template_updates" | ||
) | ||
|
||
// A SubscriberFunc describes the function signature that a subscriber needs to | ||
// have. The channel parameter is just an arbitrary identifier string the | ||
// identities a channel. The extra parameter is can contain optional data that | ||
// was sent along with the notification. | ||
type SubscriberFunc func(channel, extra string) | ||
|
||
// SetupDatabaseListener sets up a Postgres LISTEN/NOTIFY connection and listens | ||
// on events that we have subscribers for. | ||
func SetupDatabaseListener(config configuration.Registry, subscribers map[string]SubscriberFunc) error { | ||
if len(subscribers) == 0 { | ||
return nil | ||
} | ||
|
||
dbConnectCallback := func(ev pq.ListenerEventType, err error) { | ||
switch ev { | ||
case pq.ListenerEventConnected: | ||
log.Logger().Infof("database connection for LISTEN/NOTIFY established successfully") | ||
case pq.ListenerEventDisconnected: | ||
log.Logger().Errorf("lost LISTEN/NOTIFY database connection: %+v", err) | ||
case pq.ListenerEventReconnected: | ||
log.Logger().Infof("database connection for LISTEN/NOTIFY re-established successfully") | ||
case pq.ListenerEventConnectionAttemptFailed: | ||
log.Logger().Errorf("failed to connect to database for LISTEN/NOTIFY: %+v", err) | ||
} | ||
} | ||
|
||
listener := pq.NewListener(config.GetPostgresConfigString(), config.GetPostgresListenNotifyMinReconnectInterval(), config.GetPostgresListenNotifyMaxReconnectInterval(), dbConnectCallback) | ||
|
||
// listen on every subscribed channel | ||
for channel := range subscribers { | ||
err := listener.Listen(channel) | ||
if err != nil { | ||
log.Logger().Errorf("unable to open connection to database for LISTEN/NOTIFY %v", err) | ||
return errs.Wrapf(err, "failed listen to postgres channel \"%s\"", channel) | ||
} | ||
} | ||
|
||
// asynchronously handle notifications | ||
go func() { | ||
for { | ||
select { | ||
case n := <-listener.Notify: | ||
sub, ok := subscribers[n.Channel] | ||
if ok { | ||
log.Logger().Debugf("received notification from postgres channel \"%s\": %s", n.Channel, n.Extra) | ||
sub(n.Channel, n.Extra) | ||
} | ||
case <-time.After(90 * time.Second): | ||
log.Logger().Infof("received no events for 90 seconds, checking connection") | ||
go func() { | ||
err := listener.Ping() | ||
if err != nil { | ||
log.Panic(nil, map[string]interface{}{ | ||
"err": err, | ||
}, "failed to ping for LISTEN/NOTIFY database connection") | ||
} | ||
}() | ||
} | ||
} | ||
}() | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package gormsupport_test | ||
|
||
import ( | ||
"sync" | ||
"testing" | ||
|
||
"github.com/fabric8-services/fabric8-wit/gormsupport" | ||
"github.com/fabric8-services/fabric8-wit/gormtestsupport" | ||
"github.com/fabric8-services/fabric8-wit/migration" | ||
"github.com/fabric8-services/fabric8-wit/resource" | ||
"github.com/stretchr/testify/require" | ||
"github.com/stretchr/testify/suite" | ||
) | ||
|
||
type TestListenerSuite struct { | ||
gormtestsupport.DBTestSuite | ||
} | ||
|
||
func TestListener(t *testing.T) { | ||
resource.Require(t, resource.Database) | ||
suite.Run(t, &TestListenerSuite{DBTestSuite: gormtestsupport.NewDBTestSuite()}) | ||
} | ||
|
||
func (s *TestListenerSuite) TestSetupDatabaseListener() { | ||
s.T().Run("setup listener", func(t *testing.T) { | ||
// given | ||
channelName := "f8_custom_event_channel" | ||
payload := "some additional info about the event" | ||
wg := sync.WaitGroup{} | ||
wg.Add(2) | ||
|
||
gormsupport.SetupDatabaseListener(*s.Configuration, map[string]gormsupport.SubscriberFunc{ | ||
// This is the channel we send to from this test | ||
channelName: func(channel, extra string) { | ||
t.Logf("received notification on channel %s: %s", channel, extra) | ||
require.Equal(t, channelName, channel) | ||
require.Equal(t, payload, extra) | ||
wg.Done() | ||
}, | ||
// This is the channel that we send to from | ||
// migration.PopulateCommonTypes() which is called by | ||
// gormtestsupport.DBTestSuite internally. | ||
gormsupport.ChanSpaceTemplateUpdates: func(channel, extra string) { | ||
t.Logf("received notification on channel %s: %s", channel, extra) | ||
require.Equal(t, gormsupport.ChanSpaceTemplateUpdates, channel) | ||
require.Equal(t, "", extra) | ||
wg.Done() | ||
}, | ||
}) | ||
|
||
// Send a notification from a completely different connection than the | ||
// one we established to listen to channels. | ||
s.DB.Debug().Exec("SELECT pg_notify($1, $2)", channelName, payload) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mental note for myself: I need to check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in b227b9f |
||
|
||
// This will send a notification on the | ||
// gormsupport.ChanSpaceTemplateUpdates channel | ||
migration.PopulateCommonTypes(nil, s.DB) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mental note for myself: I need to check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in b227b9f |
||
|
||
// wait until notification was received | ||
wg.Wait() | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,9 @@ import ( | |
"runtime" | ||
"time" | ||
|
||
"github.com/fabric8-services/fabric8-wit/gormsupport" | ||
"github.com/fabric8-services/fabric8-wit/workitem" | ||
|
||
"github.com/fabric8-services/fabric8-wit/closeable" | ||
|
||
"github.com/fabric8-services/fabric8-wit/account" | ||
|
@@ -140,6 +143,13 @@ func main() { | |
os.Exit(0) | ||
} | ||
|
||
// Ensure we delete the work item cache when we receive a notification from postgres | ||
gormsupport.SetupDatabaseListener(*config, map[string]gormsupport.SubscriberFunc{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. I will have a look when this PR becomes more relevant. |
||
gormsupport.ChanSpaceTemplateUpdates: func(channel, extra string) { | ||
workitem.ClearGlobalWorkItemTypeCache() | ||
}, | ||
}) | ||
|
||
// Make sure the database is populated with the correct types (e.g. bug etc.) | ||
if config.GetPopulateCommonTypes() { | ||
ctx := migration.NewMigrationContext(context.Background()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The extra parameter
iscan contain