Skip to content

Commit ead2f44

Browse files
Merge pull request #12 from IBM/saikumar1607-202109270454
Optional support for Persistent caching
2 parents 100c957 + e06d7a1 commit ead2f44

File tree

11 files changed

+203
-189
lines changed

11 files changed

+203
-189
lines changed

.secrets.baseline

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "^.secrets.baseline$|go.sum|examples/SampleApp/go.sum|vendor",
44
"lines": null
55
},
6-
"generated_at": "2021-09-09T11:47:12Z",
6+
"generated_at": "2021-09-27T04:56:24Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -70,7 +70,7 @@
7070
"hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030",
7171
"is_secret": false,
7272
"is_verified": false,
73-
"line_number": 74,
73+
"line_number": 75,
7474
"type": "Secret Keyword",
7575
"verified_result": null
7676
}
@@ -86,7 +86,7 @@
8686
}
8787
]
8888
},
89-
"version": "0.13.1+ibm.45.dss",
89+
"version": "0.13.1+ibm.46.dss",
9090
"word_list": {
9191
"file": null,
9292
"hash": null

README.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ properties for distributed applications centrally.
2525

2626
## Installation
2727

28-
The current version of this SDK: 0.2.0
28+
The current version of this SDK: 0.2.1
2929

3030
There are a few different ways to download and install the IBM App Configuration Go SDK project for use by your Go
3131
application:
@@ -74,24 +74,46 @@ appConfiguration.SetContext(collectionId, environmentId)
7474
* collectionId: Id of the collection created in App Configuration service instance under the **Collections** section.
7575
* environmentId: Id of the environment created in App Configuration service instance under the **Environments** section.
7676

77-
> Here, by default live update from the server is enabled. To turn off this mode see the [below section](#work-offline-with-local-configuration-file-optional)
77+
### (Optional)
7878

79-
### Work offline with local configuration file (Optional)
79+
In order for your application and SDK to continue its operations even during the unlikely scenario of App Configuration
80+
service across your application restarts, you can configure the SDK to work using a persistent cache. The SDK uses the
81+
persistent cache to store the App Configuration data that will be available across your application restarts.
8082

81-
You can also work offline with local configuration file and perform feature and property related operations.
83+
```go
84+
// 1. default (without persistent cache)
85+
appConfiguration.SetContext(collectionId, environmentId)
86+
87+
// 2. with persistent cache
88+
appConfiguration.SetContext(collectionId, environmentId, AppConfiguration.ContextOptions{
89+
PersistentCacheDirectory: "/var/lib/docker/volumes/",
90+
})
91+
```
92+
93+
* PersistentCacheDirectory: Absolute path to a directory which has read & write permission for the user. The SDK will
94+
create a file - `AppConfiguration.json` in the specified directory, and it will be used as the persistent cache to
95+
store the App Configuration service information.
96+
97+
When persistent cache is enabled, the SDK will keep the last known good configuration at the persistent cache. In the
98+
case of App Configuration server being unreachable, the latest configurations at the persistent cache is loaded to the
99+
application to continue working.
100+
101+
### (Optional)
82102

83-
After [`appConfiguration.Init("region", "guid", "apikey")`](#using-the-sdk), follow the below steps
103+
The SDK is also designed to serve configurations, perform feature flag & property evaluations without being connected to
104+
App Configuration service.
84105

85106
```go
86-
appConfiguration.SetContext("airlines-webapp", "dev", AppConfiguration.ContextOptions{
87-
ConfigurationFile: "saflights/flights.json",
88-
LiveConfigUpdateEnabled: false
107+
appConfiguration.SetContext(collectionId, environmentId, AppConfiguration.ContextOptions{
108+
BootstrapFile: "saflights/flights.json",
109+
LiveConfigUpdateEnabled: false,
89110
})
90111
```
91112

92-
- ConfigurationFile: Path to the JSON file, which contains configuration details.
93-
- LiveConfigUpdateEnabled: Set this value to `false` if the new configuration values shouldn't be fetched from the
94-
server. Make sure to provide a proper JSON file in the path. By default, this value is enabled.
113+
* BootstrapFile: Absolute path of the JSON file, which contains configuration details. Make sure to provide a proper
114+
JSON file. You can generate this file using `ibmcloud ac config` command of the IBM Cloud App Configuration CLI.
115+
* LiveConfigUpdateEnabled: Live configuration update from the server. Set this value to `false` if the new configuration
116+
values shouldn't be fetched from the server. By default, this value is enabled.
95117

96118
## Get single feature
97119

examples/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ module examples
33
go 1.16
44

55
require (
6-
github.com/IBM/appconfiguration-go-sdk v0.2.0
6+
github.com/IBM/appconfiguration-go-sdk v0.2.1
77
github.com/gorilla/mux v1.7.2
88
)

lib/AppConfiguration.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ type AppConfiguration struct {
3232
configurationHandlerInstance *ConfigurationHandler
3333
}
3434

35-
// ContextOptions : Struct having configFilePath and liveUpdateFlag.
35+
// ContextOptions : Struct having PersistentCacheDirectory path, BootstrapFile (ConfigurationFile) path and LiveConfigUpdateEnabled flag.
3636
type ContextOptions struct {
37-
ConfigurationFile string
38-
LiveConfigUpdateEnabled bool
37+
PersistentCacheDirectory string
38+
BootstrapFile string
39+
ConfigurationFile string
40+
LiveConfigUpdateEnabled bool
3941
}
4042

4143
var appConfigurationInstance *AppConfiguration
@@ -103,13 +105,20 @@ func (ac *AppConfiguration) SetContext(collectionID string, environmentID string
103105
}
104106
switch len(options) {
105107
case 0:
106-
ac.configurationHandlerInstance.SetContext(collectionID, environmentID, "", true)
108+
ac.configurationHandlerInstance.SetContext(collectionID, environmentID, ContextOptions{
109+
LiveConfigUpdateEnabled: true,
110+
})
107111
case 1:
108-
if !options[0].LiveConfigUpdateEnabled && len(options[0].ConfigurationFile) == 0 {
109-
log.Error(messages.ConfigurationFileNotFoundError)
112+
var temp = options[0]
113+
if len(temp.ConfigurationFile) > 0 && len(temp.BootstrapFile) == 0 {
114+
temp.BootstrapFile = temp.ConfigurationFile
115+
log.Info(messages.ContextOptionsParameterDeprecation)
116+
}
117+
if !temp.LiveConfigUpdateEnabled && len(temp.BootstrapFile) == 0 {
118+
log.Error(messages.BootstrapFileNotFoundError)
110119
return
111120
}
112-
ac.configurationHandlerInstance.SetContext(collectionID, environmentID, options[0].ConfigurationFile, options[0].LiveConfigUpdateEnabled)
121+
ac.configurationHandlerInstance.SetContext(collectionID, environmentID, temp)
113122
default:
114123
log.Error(messages.IncorrectUsageOfContextOptions)
115124
return

lib/ConfigurationHandler.go

Lines changed: 63 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,20 @@
1717
package lib
1818

1919
import (
20+
"bytes"
2021
"encoding/json"
2122
"errors"
22-
"fmt"
23-
"net/http"
24-
"sync"
25-
"time"
26-
2723
"github.com/IBM/appconfiguration-go-sdk/lib/internal/constants"
2824
"github.com/IBM/appconfiguration-go-sdk/lib/internal/messages"
2925
"github.com/IBM/appconfiguration-go-sdk/lib/internal/models"
3026
"github.com/IBM/appconfiguration-go-sdk/lib/internal/utils"
31-
"github.com/IBM/go-sdk-core/v5/core"
32-
3327
"github.com/IBM/appconfiguration-go-sdk/lib/internal/utils/log"
28+
"github.com/IBM/go-sdk-core/v5/core"
3429
"github.com/gorilla/websocket"
30+
"net/http"
31+
"path"
32+
"sync"
33+
"time"
3534
)
3635

3736
type configurationUpdateListenerFunc func()
@@ -48,8 +47,10 @@ type ConfigurationHandler struct {
4847
appConfig *AppConfiguration
4948
cache *models.Cache
5049
configurationUpdateListener configurationUpdateListenerFunc
51-
configurationFile string
50+
persistentCacheDirectory string
51+
bootstrapFile string
5252
liveConfigUpdateEnabled bool
53+
persistentData []byte
5354
retryCount int
5455
retryInterval int64
5556
socketConnection *websocket.Conn
@@ -75,14 +76,15 @@ func (ch *ConfigurationHandler) Init(region, guid, apikey string) {
7576
}
7677

7778
// SetContext : Set Context
78-
func (ch *ConfigurationHandler) SetContext(collectionID, environmentID, configurationFile string, liveConfigUpdateEnabled bool) {
79+
func (ch *ConfigurationHandler) SetContext(collectionID, environmentID string, options ContextOptions) {
7980
ch.collectionID = collectionID
8081
ch.environmentID = environmentID
8182
ch.urlBuilder = utils.GetInstance()
8283
ch.urlBuilder.Init(ch.collectionID, ch.environmentID, ch.region, ch.guid, ch.apikey, OverrideServerHost)
8384
utils.GetMeteringInstance().Init(ch.guid, environmentID, collectionID)
84-
ch.configurationFile = configurationFile
85-
ch.liveConfigUpdateEnabled = liveConfigUpdateEnabled
85+
ch.persistentCacheDirectory = options.PersistentCacheDirectory
86+
ch.bootstrapFile = options.BootstrapFile
87+
ch.liveConfigUpdateEnabled = options.LiveConfigUpdateEnabled
8688
ch.isInitialized = true
8789
ch.retryCount = 3
8890
ch.retryInterval = 600
@@ -91,14 +93,31 @@ func (ch *ConfigurationHandler) loadData() {
9193
if !ch.isInitialized {
9294
log.Error(messages.ConfigurationHandlerInitError)
9395
}
94-
log.Debug(messages.LoadingData)
95-
log.Debug(messages.CheckConfigurationFileProvided)
96-
if len(ch.configurationFile) > 0 {
97-
log.Debug(messages.ConfigurationFileProvided)
98-
ch.getFileData(ch.configurationFile)
96+
if len(ch.persistentCacheDirectory) > 0 {
97+
ch.persistentData = utils.ReadFiles(path.Join(ch.persistentCacheDirectory, constants.ConfigurationFile))
98+
if !bytes.Equal(ch.persistentData, []byte(`{}`)) {
99+
// no updating the listener here. Only updating cache is enough
100+
ch.saveInCache(ch.persistentData)
101+
}
102+
}
103+
if len(ch.bootstrapFile) > 0 {
104+
log.Debug(messages.BootstrapFileProvided)
105+
if len(ch.persistentCacheDirectory) > 0 {
106+
if bytes.Equal(ch.persistentData, []byte(`{}`)) {
107+
bootstrapFileData := utils.ReadFiles(ch.bootstrapFile)
108+
go utils.StoreFiles(string(bootstrapFileData), ch.persistentCacheDirectory)
109+
ch.updateCacheAndListener(bootstrapFileData)
110+
} else {
111+
// update the only listener here. Because, cache is already updated above (line 100)
112+
if ch.configurationUpdateListener != nil {
113+
ch.configurationUpdateListener()
114+
}
115+
}
116+
} else {
117+
bootstrapFileData := utils.ReadFiles(ch.bootstrapFile)
118+
ch.updateCacheAndListener(bootstrapFileData)
119+
}
99120
}
100-
log.Debug(messages.LiveUpdateCheck)
101-
log.Debug(fmt.Sprint(ch.liveConfigUpdateEnabled))
102121
if ch.liveConfigUpdateEnabled {
103122
ch.FetchConfigurationData()
104123
}
@@ -112,11 +131,11 @@ func (ch *ConfigurationHandler) FetchConfigurationData() {
112131
go ch.startWebSocket()
113132
}
114133
}
115-
func (ch *ConfigurationHandler) saveInCache(data string) {
134+
func (ch *ConfigurationHandler) saveInCache(data []byte) {
116135
ch.mu.Lock()
117136
defer ch.mu.Unlock()
118137
configResponse := models.ConfigResponse{}
119-
err := json.Unmarshal([]byte(data), &configResponse)
138+
err := json.Unmarshal(data, &configResponse)
120139
if err != nil {
121140
log.Error(messages.UnmarshalJSONErr, err)
122141
return
@@ -140,6 +159,12 @@ func (ch *ConfigurationHandler) saveInCache(data string) {
140159
models.SetCache(featureMap, propertyMap, segmentMap)
141160
ch.cache = models.GetCacheInstance()
142161
}
162+
func (ch *ConfigurationHandler) updateCacheAndListener(data []byte) {
163+
ch.saveInCache(data)
164+
if ch.configurationUpdateListener != nil {
165+
ch.configurationUpdateListener()
166+
}
167+
}
143168
func (ch *ConfigurationHandler) fetchFromAPI() {
144169
if ch.isInitialized {
145170
ch.retryCount--
@@ -159,14 +184,24 @@ func (ch *ConfigurationHandler) fetchFromAPI() {
159184
if response != nil && response.StatusCode >= 200 && response.StatusCode <= 299 {
160185
if ch.liveConfigUpdateEnabled {
161186
jsonData, _ := json.Marshal(response.Result)
162-
ch.saveInCache(string(jsonData))
163-
if ch.configurationUpdateListener != nil {
164-
ch.configurationUpdateListener()
187+
// asynchronously write the response to persistent volume, if enabled
188+
if len(ch.persistentCacheDirectory) > 0 {
189+
go utils.StoreFiles(string(jsonData), ch.persistentCacheDirectory)
165190
}
191+
// load the configurations in the response to cache maps
192+
ch.updateCacheAndListener(jsonData)
166193
}
167194
} else {
168195
if ch.retryCount > 0 {
169-
log.Error(messages.ConfigAPIError)
196+
if response != nil {
197+
if response.Result != nil {
198+
log.Error(response.Result)
199+
} else {
200+
log.Error(string(response.RawResult))
201+
}
202+
} else {
203+
log.Error(messages.ConfigAPIError)
204+
}
170205
ch.fetchFromAPI()
171206
} else {
172207
ch.retryCount = 3
@@ -219,18 +254,6 @@ func (ch *ConfigurationHandler) startWebSocket() {
219254
}
220255
}
221256
}()
222-
223-
}
224-
225-
func (ch *ConfigurationHandler) getFeatureActions(featureID string) (models.Feature, error) {
226-
if ch.cache != nil && len(ch.cache.FeatureMap) > 0 {
227-
if val, ok := ch.cache.FeatureMap[featureID]; ok {
228-
return val, nil
229-
}
230-
log.Error(messages.InvalidFeatureID, featureID)
231-
return models.Feature{}, errors.New(messages.ErrorInvalidFeatureID + featureID)
232-
}
233-
return models.Feature{}, errors.New(messages.ErrorInvalidFeatureID + featureID)
234257
}
235258
func (ch *ConfigurationHandler) getFeatures() (map[string]models.Feature, error) {
236259
if ch.cache == nil {
@@ -243,21 +266,10 @@ func (ch *ConfigurationHandler) getFeature(featureID string) (models.Feature, er
243266
if val, ok := ch.cache.FeatureMap[featureID]; ok {
244267
return val, nil
245268
}
246-
return ch.getFeatureActions(featureID)
247269
}
248-
return ch.getFeatureActions(featureID)
249-
250-
}
270+
log.Error(messages.InvalidFeatureID, featureID)
271+
return models.Feature{}, errors.New(messages.ErrorInvalidFeatureID + featureID)
251272

252-
func (ch *ConfigurationHandler) getPropertyActions(propertyID string) (models.Property, error) {
253-
if ch.cache != nil && len(ch.cache.PropertyMap) > 0 {
254-
if val, ok := ch.cache.PropertyMap[propertyID]; ok {
255-
return val, nil
256-
}
257-
log.Error(messages.InvalidPropertyID, propertyID)
258-
return models.Property{}, errors.New(messages.ErrorInvalidPropertyID + propertyID)
259-
}
260-
return models.Property{}, errors.New(messages.ErrorInvalidPropertyID + propertyID)
261273
}
262274
func (ch *ConfigurationHandler) getProperties() (map[string]models.Property, error) {
263275
if ch.cache == nil {
@@ -270,9 +282,9 @@ func (ch *ConfigurationHandler) getProperty(propertyID string) (models.Property,
270282
if val, ok := ch.cache.PropertyMap[propertyID]; ok {
271283
return val, nil
272284
}
273-
return ch.getPropertyActions(propertyID)
274285
}
275-
return ch.getPropertyActions(propertyID)
286+
log.Error(messages.InvalidPropertyID, propertyID)
287+
return models.Property{}, errors.New(messages.ErrorInvalidPropertyID + propertyID)
276288
}
277289

278290
func (ch *ConfigurationHandler) registerConfigurationUpdateListener(chl configurationUpdateListenerFunc) {
@@ -287,23 +299,3 @@ func (ch *ConfigurationHandler) registerConfigurationUpdateListener(chl configur
287299
log.Error(messages.CollectionIDError)
288300
}
289301
}
290-
291-
func (ch *ConfigurationHandler) getFileData(filePath string) {
292-
data := utils.ReadFiles(filePath)
293-
configResp := models.ConfigResponse{}
294-
err := json.Unmarshal(data, &configResp)
295-
if err != nil {
296-
log.Error(messages.UnmarshalJSONErr, err)
297-
return
298-
}
299-
log.Debug(fmt.Sprint(configResp))
300-
out, err := json.Marshal(configResp)
301-
if err != nil {
302-
log.Error(messages.MarshalJSONErr, err)
303-
return
304-
}
305-
ch.saveInCache(string(out))
306-
if ch.configurationUpdateListener != nil {
307-
ch.configurationUpdateListener()
308-
}
309-
}

0 commit comments

Comments
 (0)