├── img └── struct.jpg ├── go.mod ├── app.properties ├── go.sum ├── .github ├── auto-comment.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── stale.yml ├── .dependabot └── config.yml ├── .gitignore ├── agcache ├── cache.go └── default.go ├── utils_test.go ├── log_test.go ├── json_config.go ├── utils.go ├── parser.go ├── coverage.sh ├── component_common_test.go ├── log.go ├── component_common.go ├── file_test.go ├── .travis.yml ├── start_test.go ├── start.go ├── notify_server_test.go ├── CONTRIBUTING.md ├── file.go ├── request_server_test.go ├── request_test.go ├── app_config_server_test.go ├── README.md ├── change_event.go ├── change_event_test.go ├── request.go ├── json_config_test.go ├── config_server_test.go ├── app_config_test.go ├── repository_test.go ├── componet_notify.go ├── componet_notify_test.go ├── app_config.go ├── repository.go └── LICENSE /img/struct.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodFrm/agollo/master/img/struct.jpg -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zouyx/agollo/v2 2 | 3 | require github.com/tevid/gohamcrest v1.1.1 -------------------------------------------------------------------------------- /app.properties: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "test", 3 | "cluster": "dev", 4 | "namespaceName": "application,abc1", 5 | "ip": "localhost:8888", 6 | "backupConfigPath":"" 7 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tevid/gohamcrest v1.1.1 h1:ou+xSqlIw1xfGTg1uq1nif/htZ2S3EzRqLm2BP+tYU0= 2 | github.com/tevid/gohamcrest v1.1.1/go.mod h1:3UvtWlqm8j5JbwYZh80D/PVBt0mJ1eJiYgZMibh0H/k= 3 | -------------------------------------------------------------------------------- /.github/auto-comment.yml: -------------------------------------------------------------------------------- 1 | # Comment to a new issue. 2 | issueOpened: > 3 | 谢谢你提出的问题,我会在1-2日内进行查看或者回复,如果遇到节假日可能会处理较慢,敬请谅解。请确保你给了我们尽可能多的背景。 4 | 5 | pullRequestOpened: > 6 | 感谢您提出Pull Request,我会尽快Review。 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: zouyx 7 | 8 | --- 9 | 10 | **功能适用的场景?** 11 | 该功能适用于xxx场景 12 | 13 | **你需要的功能是否有关联问题?** 14 | 简明扼要地描述你需要的功能,当 [...] 15 | 16 | **你期望的解决方案是?** 17 | 我期望改造xxx后,实现xxx 18 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | # Keep package.json (& lockfiles) up to date as soon as 4 | # new versions are published to the npm registry 5 | - package_manager: "go:modules" 6 | directory: "/" 7 | update_schedule: "daily" 8 | target_branch: "develop" 9 | default_assignees: 10 | - "zouyx" 11 | default_labels: 12 | - "dependencies" 13 | - "dependabot" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | /.idea/ 16 | /apolloConfig.json 17 | /application.json 18 | application*.json 19 | /abc1.json 20 | /env_test.properties 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: zouyx 7 | 8 | --- 9 | 10 | **请简单描述bug的场景** 11 | 12 | 13 | **如何重现** 14 | 15 | 再现行为的步骤: 16 | 17 | 1.跳到“…” 18 | 19 | 2.点击“…” 20 | 21 | 3.向下滚动到“…” 22 | 23 | 4.见错误 24 | 25 | **期望** 26 | 请描述期望发生什么 27 | 28 | **截图** 29 | 如有 30 | 31 | **Agollo 版本 :** 32 | - Version [e.g. 22] 33 | 34 | **日志信息** 35 | 36 | **其他上下文** 37 | 如有 38 | -------------------------------------------------------------------------------- /agcache/cache.go: -------------------------------------------------------------------------------- 1 | package agcache 2 | 3 | //CacheInterface 自定义缓存组件接口 4 | type CacheInterface interface { 5 | Set(key string, value []byte, expireSeconds int) (err error) 6 | 7 | EntryCount() (entryCount int64) 8 | 9 | Get(key string) (value []byte, err error) 10 | 11 | Del(key string) (affected bool) 12 | 13 | Range(f func(key, value interface{}) bool) 14 | 15 | Clear() 16 | } 17 | 18 | //CacheFactory 缓存组件工厂接口 19 | type CacheFactory interface { 20 | //Create 创建缓存组件 21 | Create() CacheInterface 22 | } -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | . "github.com/tevid/gohamcrest" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestGetInternal(t *testing.T) { 10 | //fmt.Println("Usage of ./getmyip --get_ip=(external|internal)") 11 | //flag.Parse() 12 | ip := getInternal() 13 | 14 | t.Log("Internal ip:", ip) 15 | nums := strings.Split(ip, ".") 16 | 17 | Assert(t, true, Equal(len(nums) > 0)) 18 | } 19 | 20 | func TestIsNotNil(t *testing.T) { 21 | flag := isNotNil(nil) 22 | Assert(t, false, Equal(flag)) 23 | 24 | flag = isNotNil("") 25 | Assert(t, true, Equal(flag)) 26 | } 27 | -------------------------------------------------------------------------------- /log_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import "testing" 4 | 5 | func TestDebugf(t *testing.T) { 6 | logger.Debugf("") 7 | } 8 | 9 | func TestInfof(t *testing.T) { 10 | logger.Infof("") 11 | } 12 | 13 | func TestErrorf(t *testing.T) { 14 | logger.Errorf("") 15 | } 16 | 17 | func TestWarnf(t *testing.T) { 18 | logger.Warnf("") 19 | } 20 | 21 | func TestDebug(t *testing.T) { 22 | logger.Debug("") 23 | } 24 | 25 | func TestInfo(t *testing.T) { 26 | logger.Info("") 27 | } 28 | 29 | func TestError(t *testing.T) { 30 | logger.Error("") 31 | } 32 | 33 | func TestWarn(t *testing.T) { 34 | logger.Warn("") 35 | } 36 | 37 | func TestInitLogger(t *testing.T) { 38 | initLogger(logger) 39 | } -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 30 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false -------------------------------------------------------------------------------- /json_config.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | ) 8 | 9 | var ( 10 | default_cluster = "default" 11 | default_namespace = "application" 12 | ) 13 | 14 | func loadJsonConfig(fileName string) (*AppConfig, error) { 15 | fs, err := ioutil.ReadFile(fileName) 16 | if err != nil { 17 | return nil, errors.New("Fail to read config file:" + err.Error()) 18 | } 19 | 20 | appConfig, loadErr := createAppConfigWithJson(string(fs)) 21 | 22 | if isNotNil(loadErr) { 23 | return nil, errors.New("Load Json Config fail:" + loadErr.Error()) 24 | } 25 | 26 | return appConfig, nil 27 | } 28 | 29 | func createAppConfigWithJson(str string) (*AppConfig, error) { 30 | appConfig := &AppConfig{ 31 | Cluster: default_cluster, 32 | NamespaceName: default_namespace, 33 | } 34 | err := json.Unmarshal([]byte(str), appConfig) 35 | if isNotNil(err) { 36 | return nil, err 37 | } 38 | 39 | return appConfig, nil 40 | } 41 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "reflect" 7 | ) 8 | 9 | var ( 10 | internalIp string 11 | ) 12 | 13 | //ips 14 | func getInternal() string { 15 | if internalIp != "" { 16 | return internalIp 17 | } 18 | 19 | addrs, err := net.InterfaceAddrs() 20 | if err != nil { 21 | os.Stderr.WriteString("Oops:" + err.Error()) 22 | os.Exit(1) 23 | } 24 | for _, a := range addrs { 25 | if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 26 | if ipnet.IP.To4() != nil { 27 | internalIp = ipnet.IP.To4().String() 28 | return internalIp 29 | } 30 | } 31 | } 32 | return "" 33 | } 34 | 35 | func isNotNil(object interface{}) bool { 36 | return !isNilObject(object) 37 | } 38 | 39 | func isNilObject(object interface{}) bool { 40 | if object == nil { 41 | return true 42 | } 43 | 44 | value := reflect.ValueOf(object) 45 | kind := value.Kind() 46 | if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { 47 | return true 48 | } 49 | 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "fmt" 5 | "github.com/zouyx/agollo/v2/agcache" 6 | ) 7 | 8 | const propertiesFormat ="%s=%s\n" 9 | 10 | //ContentParser 内容转换 11 | type ContentParser interface { 12 | parse(cache agcache.CacheInterface) (string,error) 13 | } 14 | 15 | //DefaultParser 默认内容转换器 16 | type DefaultParser struct { 17 | 18 | } 19 | 20 | func (d *DefaultParser)parse(cache agcache.CacheInterface) (string,error){ 21 | value, err := cache.Get(defaultContentKey) 22 | if err!=nil{ 23 | return "",err 24 | } 25 | return string(value),nil 26 | } 27 | 28 | //PropertiesParser properties转换器 29 | type PropertiesParser struct { 30 | 31 | } 32 | 33 | func (d *PropertiesParser)parse(cache agcache.CacheInterface) (string,error){ 34 | properties := convertToProperties(cache) 35 | return properties,nil 36 | } 37 | 38 | func convertToProperties(cache agcache.CacheInterface) string { 39 | properties:="" 40 | if cache==nil { 41 | return properties 42 | } 43 | cache.Range(func(key, value interface{}) bool { 44 | properties+=fmt.Sprintf(propertiesFormat,key,string(value.([]byte))) 45 | return true 46 | }) 47 | return properties 48 | } -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | workdir=.cover 4 | profile="$workdir/cover.out" 5 | mode=count 6 | 7 | generate_cover_data() { 8 | rm -rf "$workdir" 9 | mkdir "$workdir" 10 | 11 | for pkg in "$@"; do 12 | f="$workdir/$(echo $pkg | tr / -).cover" 13 | go test -covermode="$mode" -coverprofile="$f" "$pkg" 14 | done 15 | 16 | echo "mode: $mode" >"$profile" 17 | grep -h -v "^mode:" "$workdir"/*.cover >>"$profile" 18 | } 19 | 20 | show_html_report() { 21 | go tool cover -html="$profile" -o="$workdir"/coverage.html 22 | } 23 | 24 | show_csv_report() { 25 | go tool cover -func="$profile" -o="$workdir"/coverage.csv 26 | } 27 | 28 | push_to_coveralls() { 29 | echo "Pushing coverage statistics to coveralls.io" 30 | # ignore failure to push - it happens 31 | $HOME/gopath/bin/goveralls -coverprofile="$profile" \ 32 | -service=travis-ci || true 33 | } 34 | 35 | generate_cover_data $(go list ./...) 36 | show_csv_report 37 | 38 | case "$1" in 39 | "") 40 | ;; 41 | --html) 42 | show_html_report ;; 43 | --coveralls) 44 | push_to_coveralls ;; 45 | *) 46 | echo >&2 "error: invalid option: $1"; exit 1 ;; 47 | esac -------------------------------------------------------------------------------- /component_common_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | . "github.com/tevid/gohamcrest" 5 | "testing" 6 | ) 7 | 8 | func TestCreateApolloConfigWithJson(t *testing.T) { 9 | jsonStr := `{ 10 | "appId": "100004458", 11 | "cluster": "default", 12 | "namespaceName": "application", 13 | "configurations": { 14 | "key1":"value1", 15 | "key2":"value2" 16 | }, 17 | "releaseKey": "20170430092936-dee2d58e74515ff3" 18 | }` 19 | 20 | config, err := createApolloConfigWithJson([]byte(jsonStr)) 21 | 22 | Assert(t, err,NilVal()) 23 | Assert(t, config,NotNilVal()) 24 | 25 | Assert(t, "100004458", Equal(config.AppId)) 26 | Assert(t, "default", Equal(config.Cluster)) 27 | Assert(t, "application", Equal(config.NamespaceName)) 28 | Assert(t, "20170430092936-dee2d58e74515ff3", Equal(config.ReleaseKey)) 29 | Assert(t, "value1", Equal(config.Configurations["key1"])) 30 | Assert(t, "value2", Equal(config.Configurations["key2"])) 31 | 32 | } 33 | 34 | func TestCreateApolloConfigWithJsonError(t *testing.T) { 35 | jsonStr := `jklasdjflasjdfa` 36 | 37 | config, err := createApolloConfigWithJson([]byte(jsonStr)) 38 | 39 | Assert(t, err,NotNilVal()) 40 | Assert(t, config,NilVal()) 41 | } 42 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | var logger LoggerInterface 4 | 5 | func init() { 6 | logger=&DefaultLogger{} 7 | } 8 | 9 | func initLogger(ILogger LoggerInterface) { 10 | logger = ILogger 11 | } 12 | 13 | type LoggerInterface interface { 14 | Debugf(format string, params ...interface{}) 15 | 16 | Infof(format string, params ...interface{}) 17 | 18 | Warnf(format string, params ...interface{}) error 19 | 20 | Errorf(format string, params ...interface{}) error 21 | 22 | Debug(v ...interface{}) 23 | 24 | Info(v ...interface{}) 25 | 26 | Warn(v ...interface{}) error 27 | 28 | Error(v ...interface{}) error 29 | } 30 | 31 | type DefaultLogger struct { 32 | } 33 | 34 | func (this *DefaultLogger)Debugf(format string, params ...interface{}) { 35 | 36 | } 37 | 38 | func (this *DefaultLogger)Infof(format string, params ...interface{}) { 39 | 40 | } 41 | 42 | 43 | func (this *DefaultLogger)Warnf(format string, params ...interface{}) error { 44 | return nil 45 | } 46 | 47 | func (this *DefaultLogger)Errorf(format string, params ...interface{}) error { 48 | return nil 49 | } 50 | 51 | 52 | func (this *DefaultLogger)Debug(v ...interface{}) { 53 | 54 | } 55 | func (this *DefaultLogger)Info(v ...interface{}){ 56 | 57 | } 58 | 59 | func (this *DefaultLogger)Warn(v ...interface{}) error{ 60 | return nil 61 | } 62 | 63 | func (this *DefaultLogger)Error(v ...interface{}) error{ 64 | return nil 65 | } -------------------------------------------------------------------------------- /agcache/default.go: -------------------------------------------------------------------------------- 1 | package agcache 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | ) 7 | 8 | //DefaultCache 默认缓存 9 | type DefaultCache struct { 10 | defaultCache sync.Map 11 | } 12 | 13 | //Set 获取缓存 14 | func (d *DefaultCache)Set(key string, value []byte, expireSeconds int) (err error) { 15 | d.defaultCache.Store(key,value) 16 | return nil 17 | } 18 | 19 | //EntryCount 获取实体数量 20 | func (d *DefaultCache)EntryCount() (entryCount int64){ 21 | count:=int64(0) 22 | d.defaultCache.Range(func(key, value interface{}) bool { 23 | count++ 24 | return true 25 | }) 26 | return count 27 | } 28 | 29 | //Get 获取缓存 30 | func (d *DefaultCache)Get(key string) (value []byte, err error){ 31 | v, ok := d.defaultCache.Load(key) 32 | if !ok{ 33 | return nil,errors.New("load default cache fail") 34 | } 35 | return v.([]byte),nil 36 | } 37 | 38 | //Range 遍历缓存 39 | func (d *DefaultCache)Range(f func(key, value interface{}) bool){ 40 | d.defaultCache.Range(f) 41 | } 42 | 43 | //Del 删除缓存 44 | func (d *DefaultCache)Del(key string) (affected bool) { 45 | d.defaultCache.Delete(key) 46 | return true 47 | } 48 | 49 | //Clear 清除所有缓存 50 | func (d *DefaultCache)Clear() { 51 | d.defaultCache=sync.Map{} 52 | } 53 | 54 | //DefaultCacheFactory 构造默认缓存组件工厂类 55 | type DefaultCacheFactory struct { 56 | 57 | } 58 | 59 | //Create 创建默认缓存组件 60 | func (d *DefaultCacheFactory) Create()CacheInterface { 61 | return &DefaultCache{} 62 | } 63 | 64 | -------------------------------------------------------------------------------- /component_common.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | const ( 10 | comma = "," 11 | ) 12 | 13 | type AbsComponent interface { 14 | Start() 15 | } 16 | 17 | func StartRefreshConfig(component AbsComponent) { 18 | component.Start() 19 | } 20 | 21 | type ApolloConnConfig struct { 22 | AppId string `json:"appId"` 23 | Cluster string `json:"cluster"` 24 | NamespaceName string `json:"namespaceName"` 25 | ReleaseKey string `json:"releaseKey"` 26 | sync.RWMutex 27 | } 28 | 29 | type ApolloConfig struct { 30 | ApolloConnConfig 31 | Configurations map[string]string `json:"configurations"` 32 | } 33 | 34 | func splitNamespaces(namespacesStr string,callback func(namespace string))map[string]int64{ 35 | namespaces:=make(map[string]int64,1) 36 | split := strings.Split(namespacesStr, comma) 37 | for _, namespace := range split { 38 | callback(namespace) 39 | namespaces[namespace]=default_notification_id 40 | } 41 | return namespaces 42 | } 43 | 44 | func (a *ApolloConfig) init(appConfig *AppConfig,namespace string) { 45 | a.AppId = appConfig.AppId 46 | a.Cluster = appConfig.Cluster 47 | a.NamespaceName = namespace 48 | } 49 | 50 | func createApolloConfigWithJson(b []byte) (*ApolloConfig, error) { 51 | apolloConfig := &ApolloConfig{} 52 | err := json.Unmarshal(b, apolloConfig) 53 | if isNotNil(err) { 54 | return nil, err 55 | } 56 | return apolloConfig, nil 57 | } 58 | -------------------------------------------------------------------------------- /file_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | . "github.com/tevid/gohamcrest" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestWriteConfigFile(t *testing.T) { 10 | configPath := "" 11 | jsonStr := `{ 12 | "appId": "100004458", 13 | "cluster": "default", 14 | "namespaceName": "application", 15 | "configurations": { 16 | "key1":"value1", 17 | "key2":"value2" 18 | }, 19 | "releaseKey": "20170430092936-dee2d58e74515ff3" 20 | }` 21 | 22 | config, err := createApolloConfigWithJson([]byte(jsonStr)) 23 | os.Remove(getConfigFile(configPath,config.NamespaceName)) 24 | 25 | Assert(t,err,NilVal()) 26 | e := writeConfigFile(config, configPath) 27 | Assert(t,e,NilVal()) 28 | } 29 | 30 | func TestLoadConfigFile(t *testing.T) { 31 | jsonStr := `{ 32 | "appId": "100004458", 33 | "cluster": "default", 34 | "namespaceName": "application", 35 | "configurations": { 36 | "key1":"value1", 37 | "key2":"value2" 38 | }, 39 | "releaseKey": "20170430092936-dee2d58e74515ff3" 40 | }` 41 | 42 | config, err := createApolloConfigWithJson([]byte(jsonStr)) 43 | 44 | Assert(t,err,NilVal()) 45 | newConfig, e := loadConfigFile("",config.NamespaceName) 46 | 47 | t.Log(newConfig) 48 | Assert(t,e,NilVal()) 49 | Assert(t, config.AppId, Equal(newConfig.AppId)) 50 | Assert(t, config.ReleaseKey, Equal(newConfig.ReleaseKey)) 51 | Assert(t, config.Cluster, Equal(newConfig.Cluster)) 52 | Assert(t, config.NamespaceName, Equal(newConfig.NamespaceName)) 53 | } 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: required 3 | go: 4 | - 1.12 5 | env: 6 | - GO111MODULE=on 7 | before_install: 8 | - go get github.com/mattn/goveralls 9 | - Project=apollo 10 | install: 11 | - go mod vendor 12 | stages: 13 | - Test 14 | - Github Release 15 | jobs: 16 | include: 17 | - stage: Test 18 | if: tag IS blank 19 | go: 1.12 20 | script: 21 | - echo "Test apps ..." 22 | - chmod u+x coverage.sh 23 | - "./coverage.sh --coveralls" 24 | - echo "Build apps ..." 25 | - go build -o "$Project" 26 | - stage: Github Release 27 | if: tag =~ ^v 28 | go: 1.12 29 | deploy: 30 | provider: releases 31 | api_key: 32 | secure: la0LmwsOECDj/ZFqeUL70WKfxkplQnLkNgLq+vtG4NpFL54fVY+LVecBXtRS1WO5TUT8VIAbBIbPtIkIsZt9knkqFFgITh0R86GQyXAnsMc1xigIh+zEly1ICy7DW91WKVE4nrKLxNqgz+C2dezvb+gD4y2ol2yN67k9WdPXdDRLore3ibeKYY4UbTjOUYfGAEMoaJoLM3bn3Lzwag2uQ3irq8dJstlf173jeEkmPpDXBuKdBWXKFPvT9f8hm3Q0kqYwNUBlLl2KdjHxfxBTfGUAo8sGRh3Hcb2NNwN65xLQQNc5gSF6r45ub2OqWIILkISV0YHfvIGeNeUgh6pgY7WRnxVTEyxJ7dw53Nb4MOyyvHe4yM3Zbf12QvsNSyizLUBVOlY65w5J/aj4AWqWnwLisS8LD+AZrvuZiYTLOa3/PnX2dTn7krgGgqThRxdm/k9ViOpn32f68N+2nrv2MkDmVsNIUcaEUb0lmgEr9mR5DLx+HZCCN2ATAxnQUKKU7fiZdoNlouBBLySdULznf3MQmwocauXH/U8vVbgzcUwIo6QT35lofOrKZsLVz8RLPjgpv5Ax8Ke2wf8isj41nv/PZNAHBoe5wGOil5ISVpbd3kfbFGrwfNTt+0M50YjrK3DyLaHFndOULj4nel91YcatEM71sscf5G66UDDvQ0I= 33 | skip_cleanup: true 34 | on: 35 | tags: true 36 | -------------------------------------------------------------------------------- /start_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | . "github.com/tevid/gohamcrest" 5 | "net/http" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestStart(t *testing.T) { 11 | 12 | handlerMap := make(map[string]func(http.ResponseWriter, *http.Request), 1) 13 | handlerMap["application"]=onlyNormalConfigResponse 14 | server := runMockConfigServer(handlerMap, onlyNormalResponse) 15 | appConfig.Ip = server.URL 16 | 17 | Start() 18 | 19 | value := getValue("key1") 20 | Assert(t, "value1", Equal(value)) 21 | } 22 | 23 | func TestStartWithMultiNamespace(t *testing.T) { 24 | t.SkipNow() 25 | initDefaultConfig() 26 | initNotifications() 27 | app1 := "abc1" 28 | 29 | handlerMap := make(map[string]func(http.ResponseWriter, *http.Request), 1) 30 | handlerMap[defaultNamespace]=onlyNormalConfigResponse 31 | handlerMap[app1]=onlyNormalSecondConfigResponse 32 | server := runMockConfigServer(handlerMap, onlyNormalTwoResponse) 33 | appConfig.Ip = server.URL 34 | 35 | Start() 36 | 37 | time.Sleep(1* time.Second) 38 | 39 | value := getValue("key1") 40 | Assert(t, "value1", Equal(value)) 41 | 42 | config := GetConfig(app1) 43 | Assert(t,config,NotNilVal()) 44 | Assert(t, config.getValue("key1-1"), Equal("value1-1")) 45 | } 46 | 47 | 48 | func TestErrorStart(t *testing.T) { 49 | server := runErrorResponse() 50 | newAppConfig := getTestAppConfig() 51 | newAppConfig.Ip = server.URL 52 | 53 | time.Sleep(1 * time.Second) 54 | 55 | Start() 56 | 57 | value := getValue("key1") 58 | Assert(t, "value1", Equal(value)) 59 | 60 | value2 := getValue("key2") 61 | Assert(t, "value2", Equal(value2)) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /start.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "github.com/zouyx/agollo/v2/agcache" 5 | ) 6 | 7 | func init() { 8 | //init config 9 | initFileConfig() 10 | 11 | initCommon() 12 | } 13 | 14 | func initCommon() { 15 | initDefaultConfig() 16 | 17 | initAllNotifications() 18 | } 19 | 20 | //InitCustomConfig init config by custom 21 | func InitCustomConfig(loadAppConfig func() (*AppConfig, error)) { 22 | 23 | initConfig(loadAppConfig) 24 | 25 | initCommon() 26 | } 27 | 28 | //start apollo 29 | func Start() error { 30 | return startAgollo() 31 | } 32 | 33 | //SetLogger 设置自定义logger组件 34 | func SetLogger(loggerInterface LoggerInterface) { 35 | if loggerInterface != nil { 36 | initLogger(loggerInterface) 37 | } 38 | } 39 | 40 | //SetCache 设置自定义cache组件 41 | func SetCache(cacheFactory *agcache.DefaultCacheFactory) { 42 | if cacheFactory != nil { 43 | initConfigCache(cacheFactory) 44 | } 45 | } 46 | 47 | //StartWithLogger 通过自定义logger启动agollo 48 | func StartWithLogger(loggerInterface LoggerInterface) error { 49 | SetLogger(loggerInterface) 50 | return startAgollo() 51 | } 52 | 53 | //StartWithCache 通过自定义cache启动agollo 54 | func StartWithCache(cacheFactory *agcache.DefaultCacheFactory) error { 55 | SetCache(cacheFactory) 56 | return startAgollo() 57 | } 58 | 59 | func startAgollo() error { 60 | //init server ip list 61 | go initServerIpList() 62 | //first sync 63 | go notifySyncConfigServices() 64 | logger.Debug("init notifySyncConfigServices finished") 65 | 66 | //start long poll sync config 67 | go StartRefreshConfig(&NotifyConfigComponent{}) 68 | 69 | logger.Info("agollo start finished ! ") 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /notify_server_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "time" 8 | ) 9 | 10 | const responseStr = `[{"namespaceName":"application","notificationId":%d}]` 11 | const responseTwoStr = `[{"namespaceName":"application","notificationId":%d},{"namespaceName":"abc1","notificationId":%d}]` 12 | 13 | var normalNotifyCount = 1 14 | 15 | //Normal response 16 | //First request will hold 5s and response http.StatusNotModified 17 | //Second request will hold 5s and response http.StatusNotModified 18 | //Second request will response [{"namespaceName":"application","notificationId":3}] 19 | func runNormalResponse() *httptest.Server { 20 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | time.Sleep(10 * time.Second) 22 | normalNotifyCount++ 23 | if normalNotifyCount%3 == 0 { 24 | w.WriteHeader(http.StatusOK) 25 | w.Write([]byte(fmt.Sprintf(responseStr, normalNotifyCount))) 26 | } else { 27 | time.Sleep(5 * time.Second) 28 | w.WriteHeader(http.StatusNotModified) 29 | } 30 | })) 31 | 32 | return ts 33 | } 34 | 35 | func onlyNormalResponse(rw http.ResponseWriter, req *http.Request) { 36 | result := fmt.Sprintf(responseStr, 3) 37 | fmt.Fprintf(rw, "%s", result) 38 | } 39 | 40 | func onlyNormalTwoResponse(rw http.ResponseWriter, req *http.Request) { 41 | result := fmt.Sprintf(responseTwoStr, 3,3) 42 | fmt.Fprintf(rw, "%s", result) 43 | } 44 | 45 | //Error response 46 | //will hold 5s and keep response 404 47 | func runErrorResponse() *httptest.Server { 48 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 | w.WriteHeader(http.StatusNotFound) 50 | })) 51 | 52 | return ts 53 | } 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to agollo 2 | 3 | Apollo is released under Apache 2.0 license, and follows a very standard Github development process, using Github tracker for issues and merging pull requests into master. If you want to contribute even something trivial please do not hesitate, but follow the guidelines below. 4 | 5 | ### Sign the Contributor License Agreement 6 | 7 | Before we accept a non-trivial patch or pull request we will need you to sign the Contributor License Agreement. Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Active contributors might be asked to join the core team, and given the ability to merge pull requests. 8 | 9 | ### Code Conventions 10 | 11 | Our code style as below 12 | 13 | * Make sure all new .go files have a simple comment with at least an `@author` tag identifying you, and preferably at least a paragraph on what the class is for. 14 | 15 | * Add yourself as an @author to the .go files that you modify substantially (more than cosmetic changes). 16 | 17 | * A few unit tests should be added for a new feature or an important bug fix. 18 | 19 | * If no-one else is using your branch, please rebase it against the current master (or other target branch in the main project). 20 | 21 | * When writing a commit message please follow these conventions: if you are fixing an existing issue, please add Fixes #XXX at the end of the commit message (where XXX is the issue number). 22 | 23 | * Use ```go fmt``` to format your code , For intellij you can use **File Watching** to trigger format , If you use other IDEs, then you may use command(```go fmt ./...```) before commit. 24 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | const suffix = ".json" 11 | 12 | var configFileMap=make(map[string]string,1) 13 | 14 | //write config to file 15 | func writeConfigFile(config *ApolloConfig, configPath string) error { 16 | if config == nil { 17 | logger.Error("apollo config is null can not write backup file") 18 | return errors.New("apollo config is null can not write backup file") 19 | } 20 | file, e := os.Create(getConfigFile(configPath,config.NamespaceName)) 21 | if e != nil { 22 | logger.Errorf("writeConfigFile fail,error:", e) 23 | return e 24 | } 25 | defer file.Close() 26 | 27 | return json.NewEncoder(file).Encode(config) 28 | } 29 | 30 | //get real config file 31 | func getConfigFile(configDir string,namespace string) string { 32 | fullPath := configFileMap[namespace] 33 | if fullPath == "" { 34 | filePath := fmt.Sprintf("%s%s", namespace, suffix) 35 | if configDir != "" { 36 | configFileMap[namespace] = fmt.Sprintf("%s/%s", configDir, filePath) 37 | } else { 38 | configFileMap[namespace] = filePath 39 | } 40 | } 41 | return configFileMap[namespace] 42 | } 43 | 44 | //load config from file 45 | func loadConfigFile(configDir string,namespace string) (*ApolloConfig, error) { 46 | configFilePath := getConfigFile(configDir,namespace) 47 | logger.Info("load config file from :", configFilePath) 48 | file, e := os.Open(configFilePath) 49 | if e != nil { 50 | logger.Errorf("loadConfigFile fail,error:", e) 51 | return nil, e 52 | } 53 | defer file.Close() 54 | config := &ApolloConfig{} 55 | e = json.NewDecoder(file).Decode(config) 56 | 57 | if e != nil { 58 | logger.Errorf("loadConfigFile fail,error:", e) 59 | return nil, e 60 | } 61 | 62 | return config, e 63 | } 64 | -------------------------------------------------------------------------------- /request_server_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "time" 7 | ) 8 | 9 | var IP1 = "localhost:7080" 10 | var IP2 = "localhost:7081" 11 | 12 | var servicesResponseStr = `[{ 13 | "appName": "APOLLO-CONFIGSERVICE", 14 | "instanceId": "10.15.128.102:apollo-configservice:8080", 15 | "homepageUrl": "http://` + IP1 + `/" 16 | }, 17 | { 18 | "appName": "APOLLO-CONFIGSERVICE", 19 | "instanceId": "10.15.88.125:apollo-configservice:8080", 20 | "homepageUrl": "http://` + IP2 + `/" 21 | }]` 22 | 23 | //Normal response 24 | func runNormalServicesResponse() *httptest.Server { 25 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | w.WriteHeader(http.StatusOK) 27 | w.Write([]byte(servicesResponseStr)) 28 | })) 29 | 30 | return ts 31 | } 32 | 33 | var normalBackupConfigCount = 0 34 | 35 | //Normal response 36 | //First request will hold 5s and response http.StatusNotModified 37 | //Second request will hold 5s and response http.StatusNotModified 38 | //Second request will response [{"namespaceName":"application","notificationId":3}] 39 | func runNormalBackupConfigResponse() *httptest.Server { 40 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 41 | normalBackupConfigCount++ 42 | if normalBackupConfigCount%3 == 0 { 43 | w.WriteHeader(http.StatusOK) 44 | w.Write([]byte(configResponseStr)) 45 | } else { 46 | time.Sleep(500 * time.Microsecond) 47 | w.WriteHeader(http.StatusBadGateway) 48 | } 49 | })) 50 | 51 | return ts 52 | } 53 | 54 | //wait long time then response 55 | func runLongTimeResponse() *httptest.Server { 56 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 57 | time.Sleep(10 * time.Second) 58 | w.WriteHeader(http.StatusOK) 59 | w.Write([]byte(configResponseStr)) 60 | })) 61 | 62 | return ts 63 | } 64 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | . "github.com/tevid/gohamcrest" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestRequestRecovery(t *testing.T) { 10 | time.Sleep(1 * time.Second) 11 | mockIpList(t) 12 | server := runNormalBackupConfigResponse() 13 | newAppConfig := getTestAppConfig() 14 | newAppConfig.Ip = server.URL 15 | 16 | appConfig := GetAppConfig(newAppConfig) 17 | urlSuffix := getConfigURLSuffix(appConfig, newAppConfig.NamespaceName) 18 | 19 | o, err := requestRecovery(appConfig, &ConnectConfig{ 20 | Uri: urlSuffix, 21 | }, &CallBack{ 22 | SuccessCallBack: autoSyncConfigServicesSuccessCallBack, 23 | }) 24 | 25 | Assert(t, err,NilVal()) 26 | Assert(t, o,NilVal()) 27 | } 28 | 29 | func TestCustomTimeout(t *testing.T) { 30 | time.Sleep(1 * time.Second) 31 | mockIpList(t) 32 | server := runLongTimeResponse() 33 | newAppConfig := getTestAppConfig() 34 | newAppConfig.Ip = server.URL 35 | 36 | startTime := time.Now().Unix() 37 | appConfig := GetAppConfig(newAppConfig) 38 | urlSuffix := getConfigURLSuffix(appConfig, newAppConfig.NamespaceName) 39 | 40 | o, err := requestRecovery(appConfig, &ConnectConfig{ 41 | Uri: urlSuffix, 42 | Timeout: 11 * time.Second, 43 | }, &CallBack{ 44 | SuccessCallBack: autoSyncConfigServicesSuccessCallBack, 45 | }) 46 | 47 | endTime := time.Now().Unix() 48 | duration := endTime - startTime 49 | t.Log("start time:", startTime) 50 | t.Log("endTime:", endTime) 51 | t.Log("duration:", duration) 52 | Assert(t, int64(10), Equal(duration)) 53 | Assert(t, err,NilVal()) 54 | Assert(t, o,NilVal()) 55 | } 56 | 57 | func mockIpList(t *testing.T) { 58 | server := runNormalServicesResponse() 59 | defer server.Close() 60 | time.Sleep(1 * time.Second) 61 | newAppConfig := getTestAppConfig() 62 | newAppConfig.Ip = server.URL 63 | 64 | err := syncServerIpList(newAppConfig) 65 | 66 | Assert(t, err,NilVal()) 67 | 68 | serverLen:=getServersLen() 69 | Assert(t, 2, Equal(serverLen)) 70 | } -------------------------------------------------------------------------------- /app_config_server_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "net/http" 5 | //"time" 6 | "net/http/httptest" 7 | ) 8 | 9 | const servicesConfigResponseStr = `[{ 10 | "appName": "APOLLO-CONFIGSERVICE", 11 | "instanceId": "10.15.128.102:apollo-configservice:8080", 12 | "homepageUrl": "http://10.15.128.102:8080/" 13 | }, 14 | { 15 | "appName": "APOLLO-CONFIGSERVICE", 16 | "instanceId": "10.15.88.125:apollo-configservice:8080", 17 | "homepageUrl": "http://10.15.88.125:8080/" 18 | }, 19 | { 20 | "appName": "APOLLO-CONFIGSERVICE", 21 | "instanceId": "10.14.0.11:apollo-configservice:8080", 22 | "homepageUrl": "http://10.14.0.11:8080/" 23 | }, 24 | { 25 | "appName": "APOLLO-CONFIGSERVICE", 26 | "instanceId": "10.14.0.193:apollo-configservice:8080", 27 | "homepageUrl": "http://10.14.0.193:8080/" 28 | }, 29 | { 30 | "appName": "APOLLO-CONFIGSERVICE", 31 | "instanceId": "10.15.128.101:apollo-configservice:8080", 32 | "homepageUrl": "http://10.15.128.101:8080/" 33 | }, 34 | { 35 | "appName": "APOLLO-CONFIGSERVICE", 36 | "instanceId": "10.14.0.192:apollo-configservice:8080", 37 | "homepageUrl": "http://10.14.0.192:8080/" 38 | }, 39 | { 40 | "appName": "APOLLO-CONFIGSERVICE", 41 | "instanceId": "10.15.88.124:apollo-configservice:8080", 42 | "homepageUrl": "http://10.15.88.124:8080/" 43 | }, 44 | { 45 | "appName": "APOLLO-CONFIGSERVICE", 46 | "instanceId": "10.15.128.103:apollo-configservice:8080", 47 | "homepageUrl": "http://10.15.128.103:8080/" 48 | }, 49 | { 50 | "appName": "APOLLO-CONFIGSERVICE", 51 | "instanceId": "localhost:apollo-configservice:8080", 52 | "homepageUrl": "http://10.14.0.12:8080/" 53 | }, 54 | { 55 | "appName": "APOLLO-CONFIGSERVICE", 56 | "instanceId": "10.14.0.194:apollo-configservice:8080", 57 | "homepageUrl": "http://10.14.0.194:8080/" 58 | } 59 | ]` 60 | 61 | //var server *http.Server 62 | 63 | //run mock config server 64 | func runMockServicesConfigServer() *httptest.Server { 65 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 66 | w.WriteHeader(http.StatusOK) 67 | w.Write([]byte(servicesConfigResponseStr)) 68 | })) 69 | 70 | return ts 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Agollo - Go Client for Apollo 2 | ================ 3 | 4 | [![golang](https://img.shields.io/badge/Language-Go-green.svg?style=flat)](https://golang.org) 5 | [![Build Status](https://travis-ci.org/zouyx/agollo.svg?branch=master)](https://travis-ci.org/zouyx/agollo) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/zouyx/agollo)](https://goreportcard.com/report/github.com/zouyx/agollo) 7 | [![codebeat badge](https://codebeat.co/badges/bc2009d6-84f1-4f11-803e-fc571a12a1c0)](https://codebeat.co/projects/github-com-zouyx-agollo-master) 8 | [![Coverage Status](https://coveralls.io/repos/github/zouyx/agollo/badge.svg?branch=master)](https://coveralls.io/github/zouyx/agollo?branch=master) 9 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 10 | [![GoDoc](http://godoc.org/github.com/zouyx/agollo?status.svg)](http://godoc.org/github.com/zouyx/agollo) 11 | [![GitHub release](https://img.shields.io/github/release/zouyx/agollo.svg)](https://github.com/zouyx/agollo/releases) 12 | [![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu) 13 | 14 | 方便Golang接入配置中心框架 [Apollo](https://github.com/ctripcorp/apollo) 所开发的Golang版本客户端。 15 | 16 | # Features 17 | * 实时同步配置 18 | * 灰度配置 19 | * 延迟加载(运行时)namespace 20 | * 客户端,配置文件容灾 21 | * 自定义日志,缓存组件 22 | 23 | # Usage 24 | 25 | ***使用Demo*** :[agollo_demo](https://github.com/zouyx/agollo_demo) 26 | 27 | ***其他语言*** : [agollo-agent](https://github.com/zouyx/agollo-agent.git) 做本地agent接入,如:PHP 28 | 29 | 欢迎查阅 [Wiki](https://github.com/zouyx/agollo/wiki) 或者 [godoc](http://godoc.org/github.com/zouyx/agollo) 获取更多有用的信息 30 | 31 | 如果你觉得该工具还不错或者有问题,一定要让我知道,可以发邮件或者[留言](https://github.com/zouyx/agollo/issues)。 32 | 33 | # User 34 | 35 | * [使用者名单](https://github.com/zouyx/agollo/issues/20) 36 | 37 | # Contribution 38 | * Source Code: https://github.com/zouyx/agollo/ 39 | * Issue Tracker: https://github.com/zouyx/agollo/issues 40 | 41 | # License 42 | The project is licensed under the [Apache 2 license](https://github.com/zouyx/agollo/blob/master/LICENSE). 43 | 44 | # Reference 45 | Apollo : https://github.com/ctripcorp/apollo 46 | -------------------------------------------------------------------------------- /change_event.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "container/list" 5 | ) 6 | 7 | const ( 8 | ADDED ConfigChangeType = iota 9 | MODIFIED 10 | DELETED 11 | ) 12 | 13 | var ( 14 | changeListeners *list.List 15 | ) 16 | 17 | func init() { 18 | changeListeners=list.New() 19 | } 20 | 21 | //ChangeListener 监听器 22 | type ChangeListener interface { 23 | //OnChange 增加变更监控 24 | OnChange(event *ChangeEvent) 25 | } 26 | 27 | 28 | //config change type 29 | type ConfigChangeType int 30 | 31 | //config change event 32 | type ChangeEvent struct { 33 | Namespace string 34 | Changes map[string]*ConfigChange 35 | } 36 | 37 | type ConfigChange struct { 38 | OldValue string 39 | NewValue string 40 | ChangeType ConfigChangeType 41 | } 42 | 43 | //AddChangeListener 增加变更监控 44 | func AddChangeListener(listener ChangeListener) { 45 | if listener==nil{ 46 | return 47 | } 48 | changeListeners.PushBack(listener) 49 | } 50 | 51 | //RemoveChangeListener 增加变更监控 52 | func removeChangeListener(listener ChangeListener) { 53 | if listener==nil{ 54 | return 55 | } 56 | for i := changeListeners.Front(); i != nil; i = i.Next() { 57 | apolloListener:= i.Value.(ChangeListener) 58 | if listener==apolloListener{ 59 | changeListeners.Remove(i) 60 | } 61 | } 62 | } 63 | 64 | //push config change event 65 | func pushChangeEvent(event *ChangeEvent) { 66 | // if channel is null ,mean no listener,don't need to push msg 67 | if changeListeners == nil||changeListeners.Len()==0 { 68 | return 69 | } 70 | 71 | for i := changeListeners.Front(); i != nil; i = i.Next() { 72 | listener:= i.Value.(ChangeListener) 73 | go listener.OnChange(event) 74 | } 75 | } 76 | 77 | //create modify config change 78 | func createModifyConfigChange(oldValue string, newValue string) *ConfigChange { 79 | return &ConfigChange{ 80 | OldValue: oldValue, 81 | NewValue: newValue, 82 | ChangeType: MODIFIED, 83 | } 84 | } 85 | 86 | //create add config change 87 | func createAddConfigChange(newValue string) *ConfigChange { 88 | return &ConfigChange{ 89 | NewValue: newValue, 90 | ChangeType: ADDED, 91 | } 92 | } 93 | 94 | //create delete config change 95 | func createDeletedConfigChange(oldValue string) *ConfigChange { 96 | return &ConfigChange{ 97 | OldValue: oldValue, 98 | ChangeType: DELETED, 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /change_event_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "container/list" 5 | "encoding/json" 6 | "fmt" 7 | . "github.com/tevid/gohamcrest" 8 | "sync" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | type CustomChangeListener struct { 14 | t *testing.T 15 | group *sync.WaitGroup 16 | } 17 | 18 | func (c *CustomChangeListener) OnChange(changeEvent *ChangeEvent) { 19 | if c.group==nil{ 20 | return 21 | } 22 | defer c.group.Done() 23 | bytes, _ := json.Marshal(changeEvent) 24 | fmt.Println("event:", string(bytes)) 25 | 26 | Assert(c.t, "application", Equal(changeEvent.Namespace)) 27 | 28 | Assert(c.t, "string", Equal(changeEvent.Changes["string"].NewValue)) 29 | Assert(c.t, "", Equal(changeEvent.Changes["string"].OldValue)) 30 | Assert(c.t, ADDED, Equal(changeEvent.Changes["string"].ChangeType)) 31 | 32 | Assert(c.t, "value1", Equal(changeEvent.Changes["key1"].NewValue)) 33 | Assert(c.t, "", Equal(changeEvent.Changes["key2"].OldValue)) 34 | Assert(c.t, ADDED, Equal(changeEvent.Changes["key1"].ChangeType)) 35 | 36 | Assert(c.t, "value2", Equal(changeEvent.Changes["key2"].NewValue)) 37 | Assert(c.t, "", Equal(changeEvent.Changes["key2"].OldValue)) 38 | Assert(c.t, ADDED, Equal(changeEvent.Changes["key2"].ChangeType)) 39 | } 40 | 41 | func TestListenChangeEvent(t *testing.T) { 42 | go buildNotifyResult(t) 43 | group:= sync.WaitGroup{} 44 | group.Add(1) 45 | 46 | listener := &CustomChangeListener{ 47 | t:t, 48 | group:&group, 49 | } 50 | AddChangeListener(listener) 51 | group.Wait() 52 | //运行完清空变更队列 53 | changeListeners=list.New() 54 | } 55 | 56 | func buildNotifyResult(t *testing.T) { 57 | server := runChangeConfigResponse() 58 | defer server.Close() 59 | 60 | time.Sleep(1 * time.Second) 61 | 62 | newAppConfig := getTestAppConfig() 63 | newAppConfig.Ip = server.URL 64 | 65 | err := autoSyncConfigServices(newAppConfig) 66 | err = autoSyncConfigServices(newAppConfig) 67 | 68 | Assert(t, err,NilVal()) 69 | 70 | config := GetCurrentApolloConfig()[newAppConfig.NamespaceName] 71 | 72 | Assert(t, "100004458", Equal(config.AppId)) 73 | Assert(t, "default", Equal(config.Cluster)) 74 | Assert(t, "application", Equal(config.NamespaceName)) 75 | Assert(t, "20170430092936-dee2d58e74515ff3", Equal(config.ReleaseKey)) 76 | } 77 | 78 | func TestRemoveChangeListener(t *testing.T) { 79 | go buildNotifyResult(t) 80 | 81 | listener := &CustomChangeListener{ 82 | } 83 | AddChangeListener(listener) 84 | Assert(t, 1, Equal(changeListeners.Len())) 85 | removeChangeListener(listener) 86 | Assert(t, 0, Equal(changeListeners.Len())) 87 | 88 | //运行完清空变更队列 89 | changeListeners=list.New() 90 | } 91 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | type CallBack struct { 12 | SuccessCallBack func([]byte) (interface{}, error) 13 | NotModifyCallBack func() error 14 | } 15 | 16 | type ConnectConfig struct { 17 | //设置到http.client中timeout字段 18 | Timeout time.Duration 19 | //连接接口的uri 20 | Uri string 21 | } 22 | 23 | func request(requestUrl string, connectionConfig *ConnectConfig, callBack *CallBack) (interface{}, error) { 24 | client := &http.Client{} 25 | //如有设置自定义超时时间即使用 26 | if connectionConfig != nil && connectionConfig.Timeout != 0 { 27 | client.Timeout = connectionConfig.Timeout 28 | } else { 29 | client.Timeout = connect_timeout 30 | } 31 | 32 | retry := 0 33 | var responseBody []byte 34 | var err error 35 | var res *http.Response 36 | for { 37 | retry++ 38 | 39 | if retry > max_retries { 40 | break 41 | } 42 | 43 | res, err = client.Get(requestUrl) 44 | 45 | if res == nil || err != nil { 46 | logger.Error("Connect Apollo Server Fail,url:%s,Error:%s",requestUrl, err) 47 | continue 48 | } 49 | 50 | //not modified break 51 | switch res.StatusCode { 52 | case http.StatusOK: 53 | responseBody, err = ioutil.ReadAll(res.Body) 54 | if err != nil { 55 | logger.Error("Connect Apollo Server Fail,url:%s,Error:",requestUrl, err) 56 | continue 57 | } 58 | 59 | if callBack != nil && callBack.SuccessCallBack != nil { 60 | return callBack.SuccessCallBack(responseBody) 61 | } else { 62 | return nil, nil 63 | } 64 | case http.StatusNotModified: 65 | logger.Info("Config Not Modified:", err) 66 | if callBack != nil && callBack.NotModifyCallBack != nil { 67 | return nil, callBack.NotModifyCallBack() 68 | } else { 69 | return nil, nil 70 | } 71 | default: 72 | logger.Error("Connect Apollo Server Fail,url:%s,Error:%s",requestUrl, err) 73 | if res != nil { 74 | logger.Error("Connect Apollo Server Fail,url:%s,StatusCode:%s",requestUrl, res.StatusCode) 75 | } 76 | err = errors.New("Connect Apollo Server Fail!") 77 | // if error then sleep 78 | time.Sleep(on_error_retry_interval) 79 | continue 80 | } 81 | } 82 | 83 | logger.Error("Over Max Retry Still Error,Error:", err) 84 | if err != nil { 85 | err = errors.New("Over Max Retry Still Error!") 86 | } 87 | return nil, err 88 | } 89 | 90 | func requestRecovery(appConfig *AppConfig, 91 | connectConfig *ConnectConfig, 92 | callBack *CallBack) (interface{}, error) { 93 | format := "%s%s" 94 | var err error 95 | var response interface{} 96 | 97 | for { 98 | host := appConfig.selectHost() 99 | if host == "" { 100 | return nil, err 101 | } 102 | 103 | requestUrl := fmt.Sprintf(format, host, connectConfig.Uri) 104 | response, err = request(requestUrl, connectConfig, callBack) 105 | if err == nil { 106 | return response, err 107 | } 108 | 109 | setDownNode(host) 110 | } 111 | 112 | return nil, errors.New("Try all Nodes Still Error!") 113 | } 114 | -------------------------------------------------------------------------------- /json_config_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "encoding/json" 5 | . "github.com/tevid/gohamcrest" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestLoadJsonConfig(t *testing.T) { 11 | config, err := loadJsonConfig(APP_CONFIG_FILE_NAME) 12 | t.Log(config) 13 | 14 | Assert(t, err,NilVal()) 15 | Assert(t, config,NotNilVal()) 16 | Assert(t, "test", Equal(config.AppId)) 17 | Assert(t, "dev", Equal(config.Cluster)) 18 | Assert(t, "application,abc1",Equal(config.NamespaceName)) 19 | Assert(t, "localhost:8888", Equal(config.Ip)) 20 | 21 | } 22 | 23 | 24 | func TestLoadEnvConfig(t *testing.T) { 25 | envConfigFile:="env_test.properties" 26 | config, _ := loadJsonConfig(APP_CONFIG_FILE_NAME) 27 | config.Ip="123" 28 | config.AppId="1111" 29 | config.NamespaceName="nsabbda" 30 | file, err := os.Create(envConfigFile) 31 | if err != nil { 32 | t.Error(err) 33 | t.FailNow() 34 | } 35 | defer file.Close() 36 | err = json.NewEncoder(file).Encode(config) 37 | if err != nil { 38 | t.Error(err) 39 | t.FailNow() 40 | } 41 | 42 | err = os.Setenv(ENV_CONFIG_FILE_PATH, envConfigFile) 43 | envConfig, envConfigErr := getLoadAppConfig(nil) 44 | t.Log(config) 45 | 46 | Assert(t, envConfigErr,NilVal()) 47 | Assert(t, envConfig,NotNilVal()) 48 | Assert(t, envConfig.AppId, Equal(config.AppId)) 49 | Assert(t, envConfig.Cluster, Equal(config.Cluster)) 50 | Assert(t, envConfig.NamespaceName,Equal(config.NamespaceName)) 51 | Assert(t, envConfig.Ip, Equal(config.Ip)) 52 | 53 | } 54 | 55 | func TestLoadJsonConfigWrongFile(t *testing.T) { 56 | config, err := loadJsonConfig("") 57 | Assert(t, err,NotNilVal()) 58 | Assert(t, config,NilVal()) 59 | 60 | Assert(t, err.Error(),StartWith("Fail to read config file", )) 61 | } 62 | 63 | func TestLoadJsonConfigWrongType(t *testing.T) { 64 | config, err := loadJsonConfig("app_config.go") 65 | Assert(t, err,NotNilVal()) 66 | Assert(t, config,NilVal()) 67 | 68 | Assert(t, err.Error(),StartWith("Load Json Config fail")) 69 | } 70 | 71 | func TestCreateAppConfigWithJson(t *testing.T) { 72 | jsonStr := `{ 73 | "appId": "test", 74 | "cluster": "dev", 75 | "namespaceName": "application", 76 | "ip": "localhost:8888", 77 | "releaseKey": "" 78 | }` 79 | config, err := createAppConfigWithJson(jsonStr) 80 | t.Log(config) 81 | 82 | Assert(t, err,NilVal()) 83 | Assert(t, config,NotNilVal()) 84 | Assert(t, "test", Equal(config.AppId)) 85 | Assert(t, "dev", Equal(config.Cluster)) 86 | Assert(t, "application", Equal(config.NamespaceName)) 87 | Assert(t, "localhost:8888", Equal(config.Ip)) 88 | } 89 | 90 | //func TestCreateAppConfigWithJsonWrongEnv(t *testing.T) { 91 | // jsonStr:=`{ 92 | // "appId": "test", 93 | // "cluster": "joe", 94 | // "namespaceName": "application", 95 | // "ip": "localhost:8888", 96 | // "releaseKey": "" 97 | // }` 98 | // config,err:=createAppConfigWithJson(jsonStr) 99 | // t.Log(config) 100 | // t.Log(err) 101 | // 102 | // Assert(t,err) 103 | // Assert(t,config) 104 | // test.StartWith(t,"Env is wrong ,current env:joe",err.Error()) 105 | //} 106 | 107 | func TestCreateAppConfigWithJsonError(t *testing.T) { 108 | jsonStr := `package agollo 109 | 110 | import ( 111 | "os" 112 | "strconv" 113 | "time" 114 | "fmt" 115 | "net/url" 116 | )` 117 | config, err := createAppConfigWithJson(jsonStr) 118 | t.Log(err) 119 | 120 | Assert(t, err,NotNilVal()) 121 | Assert(t, config,NilVal()) 122 | } 123 | 124 | func TestCreateAppConfigWithJsonDefault(t *testing.T) { 125 | jsonStr := `{ 126 | "appId": "testDefault", 127 | "ip": "localhost:9999" 128 | }` 129 | config, err := createAppConfigWithJson(jsonStr) 130 | t.Log(err) 131 | 132 | Assert(t, err,NilVal()) 133 | Assert(t, config,NotNilVal()) 134 | Assert(t, "testDefault", Equal(config.AppId)) 135 | Assert(t, "default", Equal(config.Cluster)) 136 | Assert(t, "application", Equal(config.NamespaceName)) 137 | Assert(t, "localhost:9999", Equal(config.Ip)) 138 | } 139 | -------------------------------------------------------------------------------- /config_server_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const configResponseStr = `{ 12 | "appId": "100004458", 13 | "cluster": "default", 14 | "namespaceName": "application", 15 | "configurations": { 16 | "key1":"value1", 17 | "key2":"value2" 18 | }, 19 | "releaseKey": "20170430092936-dee2d58e74515ff3" 20 | }` 21 | 22 | const configSecondResponseStr = `{ 23 | "appId": "100004459", 24 | "cluster": "default", 25 | "namespaceName": "abc1", 26 | "configurations": { 27 | "key1-1":"value1-1", 28 | "key1-2":"value2-1" 29 | }, 30 | "releaseKey": "20170430092936-dee2d58e74515ff3" 31 | }` 32 | 33 | const configChangeResponseStr = `{ 34 | "appId": "100004458", 35 | "cluster": "default", 36 | "namespaceName": "application", 37 | "configurations": { 38 | "key1":"value1", 39 | "key2":"value2", 40 | "string":"string" 41 | }, 42 | "releaseKey": "20170430092936-dee2d58e74515ff3" 43 | }` 44 | 45 | //run mock config server 46 | func runMockConfigServer(handlerMap map[string]func(http.ResponseWriter, *http.Request), 47 | notifyHandler func(http.ResponseWriter, *http.Request)) *httptest.Server{ 48 | appConfig := GetAppConfig(nil) 49 | uriHandlerMap := make(map[string]func(http.ResponseWriter, *http.Request), 0) 50 | for namespace, handler := range handlerMap { 51 | uri := fmt.Sprintf("/configs/%s/%s/%s", appConfig.AppId, appConfig.Cluster, namespace) 52 | uriHandlerMap[uri]=handler 53 | } 54 | uriHandlerMap["/notifications/v2"]=notifyHandler 55 | 56 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 57 | uri := r.RequestURI 58 | for path, handler := range uriHandlerMap { 59 | if strings.HasPrefix(uri,path){ 60 | handler(w,r) 61 | break 62 | } 63 | } 64 | })) 65 | 66 | return ts 67 | } 68 | 69 | 70 | var normalConfigCount = 1 71 | 72 | //Normal response 73 | //First request will hold 5s and response http.StatusNotModified 74 | //Second request will hold 5s and response http.StatusNotModified 75 | //Second request will response [{"namespaceName":"application","notificationId":3}] 76 | func runNormalConfigResponse() *httptest.Server { 77 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 78 | normalConfigCount++ 79 | if normalConfigCount%3 == 0 { 80 | w.WriteHeader(http.StatusOK) 81 | w.Write([]byte(configResponseStr)) 82 | } else { 83 | time.Sleep(500 * time.Microsecond) 84 | w.WriteHeader(http.StatusNotModified) 85 | } 86 | })) 87 | 88 | return ts 89 | } 90 | 91 | func runLongNotmodifiedConfigResponse() *httptest.Server { 92 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 93 | time.Sleep(500 * time.Microsecond) 94 | w.WriteHeader(http.StatusNotModified) 95 | })) 96 | 97 | return ts 98 | } 99 | 100 | func runChangeConfigResponse() *httptest.Server { 101 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 102 | w.WriteHeader(http.StatusOK) 103 | w.Write([]byte(configChangeResponseStr)) 104 | })) 105 | 106 | return ts 107 | } 108 | 109 | func onlyNormalConfigResponse(rw http.ResponseWriter, req *http.Request) { 110 | rw.WriteHeader(http.StatusOK) 111 | fmt.Fprintf(rw, configResponseStr) 112 | } 113 | 114 | func onlyNormalSecondConfigResponse(rw http.ResponseWriter, req *http.Request) { 115 | rw.WriteHeader(http.StatusOK) 116 | fmt.Fprintf(rw, configSecondResponseStr) 117 | } 118 | 119 | func runNotModifyConfigResponse() *httptest.Server { 120 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 121 | time.Sleep(800 * time.Microsecond) 122 | w.WriteHeader(http.StatusNotModified) 123 | })) 124 | 125 | return ts 126 | } 127 | 128 | //Error response 129 | //will hold 5s and keep response 404 130 | func runErrorConfigResponse() *httptest.Server { 131 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 132 | time.Sleep(500 * time.Microsecond) 133 | w.WriteHeader(http.StatusNotFound) 134 | })) 135 | 136 | return ts 137 | } 138 | -------------------------------------------------------------------------------- /app_config_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | . "github.com/tevid/gohamcrest" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestInit(t *testing.T) { 10 | config := GetAppConfig(nil) 11 | time.Sleep(1 * time.Second) 12 | 13 | Assert(t, config, NotNilVal()) 14 | Assert(t, "test", Equal(config.AppId)) 15 | Assert(t, "dev", Equal(config.Cluster)) 16 | Assert(t, "application,abc1", Equal(config.NamespaceName)) 17 | Assert(t, "localhost:8888", Equal(config.Ip)) 18 | 19 | apolloConfig := GetCurrentApolloConfig()[defaultNamespace] 20 | Assert(t, "test", Equal(apolloConfig.AppId)) 21 | Assert(t, "dev", Equal(apolloConfig.Cluster)) 22 | Assert(t, "application", Equal(apolloConfig.NamespaceName)) 23 | 24 | } 25 | 26 | func TestStructInit(t *testing.T) { 27 | 28 | readyConfig := &AppConfig{ 29 | AppId: "test1", 30 | Cluster: "dev1", 31 | NamespaceName: "application1", 32 | Ip: "localhost:8889", 33 | } 34 | 35 | InitCustomConfig(func() (*AppConfig, error) { 36 | return readyConfig, nil 37 | }) 38 | 39 | time.Sleep(1 * time.Second) 40 | 41 | config := GetAppConfig(nil) 42 | Assert(t, config, NotNilVal()) 43 | Assert(t, "test1", Equal(config.AppId)) 44 | Assert(t, "dev1", Equal(config.Cluster)) 45 | Assert(t, "application1", Equal(config.NamespaceName)) 46 | Assert(t, "localhost:8889", Equal(config.Ip)) 47 | 48 | apolloConfig := GetCurrentApolloConfig()[config.NamespaceName] 49 | Assert(t, "test1", Equal(apolloConfig.AppId)) 50 | Assert(t, "dev1", Equal(apolloConfig.Cluster)) 51 | Assert(t, "application1", Equal(apolloConfig.NamespaceName)) 52 | 53 | //revert file config 54 | initFileConfig() 55 | } 56 | 57 | func TestGetConfigUrl(t *testing.T) { 58 | appConfig := getTestAppConfig() 59 | url := getConfigUrl(appConfig) 60 | Assert(t, url, StartWith("http://localhost:8888/configs/test/dev/application?releaseKey=&ip=")) 61 | } 62 | 63 | func TestGetConfigUrlByHost(t *testing.T) { 64 | appConfig := getTestAppConfig() 65 | url := getConfigUrlByHost(appConfig, "http://baidu.com/") 66 | Assert(t, url, StartWith("http://baidu.com/configs/test/dev/application?releaseKey=&ip=")) 67 | } 68 | 69 | func TestGetServicesConfigUrl(t *testing.T) { 70 | appConfig := getTestAppConfig() 71 | url := getServicesConfigUrl(appConfig) 72 | ip := getInternal() 73 | Assert(t, "http://localhost:8888/services/config?appId=test&ip="+ip, Equal(url)) 74 | } 75 | 76 | func getTestAppConfig() *AppConfig { 77 | jsonStr := `{ 78 | "appId": "test", 79 | "cluster": "dev", 80 | "namespaceName": "application", 81 | "ip": "localhost:8888", 82 | "releaseKey": "1" 83 | }` 84 | config, _ := createAppConfigWithJson(jsonStr) 85 | 86 | return config 87 | } 88 | 89 | func TestSyncServerIpList(t *testing.T) { 90 | trySyncServerIpList(t) 91 | } 92 | 93 | func trySyncServerIpList(t *testing.T) { 94 | server := runMockServicesConfigServer() 95 | defer server.Close() 96 | 97 | newAppConfig := getTestAppConfig() 98 | newAppConfig.Ip = server.URL 99 | err := syncServerIpList(newAppConfig) 100 | 101 | Assert(t, err, NilVal()) 102 | 103 | serverLen := getServersLen() 104 | 105 | Assert(t, 10, Equal(serverLen)) 106 | 107 | } 108 | 109 | func TestSelectHost(t *testing.T) { 110 | //mock ip data 111 | trySyncServerIpList(t) 112 | 113 | t.Log("appconfig host:" + appConfig.getHost()) 114 | t.Log("appconfig select host:" + appConfig.selectHost()) 115 | 116 | host := "http://localhost:8888/" 117 | Assert(t, host, Equal(appConfig.getHost())) 118 | Assert(t, host, Equal(appConfig.selectHost())) 119 | 120 | //check select next time 121 | appConfig.setNextTryConnTime(5) 122 | Assert(t, host, NotEqual(appConfig.selectHost())) 123 | time.Sleep(6 * time.Second) 124 | Assert(t, host, Equal(appConfig.selectHost())) 125 | 126 | //check servers 127 | appConfig.setNextTryConnTime(5) 128 | firstHost := appConfig.selectHost() 129 | Assert(t, host, NotEqual(firstHost)) 130 | setDownNode(firstHost) 131 | 132 | secondHost := appConfig.selectHost() 133 | Assert(t, host, NotEqual(secondHost)) 134 | Assert(t, firstHost, NotEqual(secondHost)) 135 | setDownNode(secondHost) 136 | 137 | thirdHost := appConfig.selectHost() 138 | Assert(t, host, NotEqual(thirdHost)) 139 | Assert(t, firstHost, NotEqual(thirdHost)) 140 | Assert(t, secondHost, NotEqual(thirdHost)) 141 | 142 | servers.Range(func(k, v interface{}) bool { 143 | setDownNode(k.(string)) 144 | return true 145 | }) 146 | 147 | Assert(t, "", Equal(appConfig.selectHost())) 148 | 149 | //no servers 150 | //servers = make(map[string]*serverInfo, 0) 151 | deleteServers() 152 | Assert(t, "", Equal(appConfig.selectHost())) 153 | } 154 | 155 | func deleteServers() { 156 | servers.Range(func(k, v interface{}) bool { 157 | servers.Delete(k) 158 | return true 159 | }) 160 | } 161 | 162 | func getServersLen() int { 163 | len := 0 164 | servers.Range(func(k, v interface{}) bool { 165 | len++ 166 | return true 167 | }) 168 | return len 169 | } 170 | -------------------------------------------------------------------------------- /repository_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "encoding/json" 5 | . "github.com/tevid/gohamcrest" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | //init param 11 | func init() { 12 | } 13 | 14 | func createMockApolloConfig(expireTime int) map[string]string { 15 | configs := make(map[string]string, 0) 16 | //string 17 | configs["string"] = "value" 18 | //int 19 | configs["int"] = "1" 20 | //float 21 | configs["float"] = "190.3" 22 | //bool 23 | configs["bool"] = "true" 24 | 25 | updateApolloConfigCache(configs, expireTime,defaultNamespace) 26 | 27 | return configs 28 | } 29 | 30 | func getFirstApolloConfig(t *testing.T,currentConfig map[string]*ApolloConnConfig)[]byte { 31 | i:=0 32 | var currentJSON []byte 33 | var err error 34 | for _, v := range currentConfig { 35 | if i > 0 { 36 | break 37 | } 38 | currentJSON, err = json.Marshal(v) 39 | i++ 40 | } 41 | Assert(t, err,NilVal()) 42 | 43 | t.Log("currentJSON:", string(currentJSON)) 44 | 45 | Assert(t, false, Equal(string(currentJSON) == "")) 46 | return currentJSON 47 | } 48 | 49 | func TestUpdateApolloConfigNull(t *testing.T) { 50 | time.Sleep(1 * time.Second) 51 | var currentConfig *ApolloConnConfig 52 | currentJSON:=getFirstApolloConfig(t,currentConnApolloConfig.configs) 53 | 54 | 55 | json.Unmarshal(currentJSON, ¤tConfig) 56 | 57 | Assert(t, currentConfig,NotNilVal()) 58 | 59 | updateApolloConfig(nil, true) 60 | 61 | currentConnApolloConfig.l.RLock() 62 | defer currentConnApolloConfig.l.RUnlock() 63 | config := currentConnApolloConfig.configs[defaultNamespace] 64 | 65 | //make sure currentConnApolloConfig was not modified 66 | //Assert(t, currentConfig.NamespaceName, config.NamespaceName) 67 | //Assert(t, currentConfig.AppId, config.AppId) 68 | //Assert(t, currentConfig.Cluster, config.Cluster) 69 | //Assert(t, currentConfig.ReleaseKey, config.ReleaseKey) 70 | Assert(t, config,NotNilVal()) 71 | Assert(t, defaultNamespace, Equal(config.NamespaceName)) 72 | Assert(t, "test", Equal(config.AppId)) 73 | Assert(t, "dev", Equal(config.Cluster)) 74 | Assert(t, "", Equal(config.ReleaseKey)) 75 | 76 | } 77 | 78 | func TestGetApolloConfigCache(t *testing.T) { 79 | cache := GetApolloConfigCache() 80 | Assert(t, cache,NotNilVal()) 81 | } 82 | 83 | func TestGetConfigValueNullApolloConfig(t *testing.T) { 84 | //clear Configurations 85 | defaultConfigCache := getDefaultConfigCache() 86 | defaultConfigCache.Clear() 87 | 88 | //test getValue 89 | value := getValue("joe") 90 | 91 | Assert(t, empty, Equal(value)) 92 | 93 | //test GetStringValue 94 | defaultValue := "j" 95 | 96 | //test default 97 | v := GetStringValue("joe", defaultValue) 98 | 99 | Assert(t, defaultValue, Equal(v)) 100 | 101 | 102 | } 103 | 104 | func TestGetIntValue(t *testing.T) { 105 | createMockApolloConfig(configCacheExpireTime) 106 | defaultValue := 100000 107 | 108 | //test default 109 | v := GetIntValue("joe", defaultValue) 110 | 111 | Assert(t, defaultValue, Equal(v)) 112 | 113 | //normal value 114 | v = GetIntValue("int", defaultValue) 115 | 116 | Assert(t, 1, Equal(v)) 117 | 118 | //error type 119 | v = GetIntValue("float", defaultValue) 120 | 121 | Assert(t, defaultValue, Equal(v)) 122 | } 123 | 124 | func TestGetFloatValue(t *testing.T) { 125 | defaultValue := 100000.1 126 | 127 | //test default 128 | v := GetFloatValue("joe", defaultValue) 129 | 130 | Assert(t, defaultValue, Equal(v)) 131 | 132 | //normal value 133 | v = GetFloatValue("float", defaultValue) 134 | 135 | Assert(t, 190.3, Equal(v)) 136 | 137 | //error type 138 | v = GetFloatValue("int", defaultValue) 139 | 140 | Assert(t, float64(1), Equal(v)) 141 | } 142 | 143 | func TestGetBoolValue(t *testing.T) { 144 | defaultValue := false 145 | 146 | //test default 147 | v := GetBoolValue("joe", defaultValue) 148 | 149 | Assert(t, defaultValue, Equal(v)) 150 | 151 | //normal value 152 | v = GetBoolValue("bool", defaultValue) 153 | 154 | Assert(t, true, Equal(v)) 155 | 156 | //error type 157 | v = GetBoolValue("float", defaultValue) 158 | 159 | Assert(t, defaultValue, Equal(v)) 160 | } 161 | 162 | func TestGetStringValue(t *testing.T) { 163 | defaultValue := "j" 164 | 165 | //test default 166 | v := GetStringValue("joe", defaultValue) 167 | 168 | Assert(t, defaultValue, Equal(v)) 169 | 170 | //normal value 171 | v = GetStringValue("string", defaultValue) 172 | 173 | Assert(t, "value", Equal(v)) 174 | } 175 | 176 | func TestConfig_GetStringValue(t *testing.T) { 177 | config := GetConfig(defaultNamespace) 178 | 179 | defaultValue := "j" 180 | //test default 181 | v:=config.GetStringValue("joe", defaultValue) 182 | Assert(t, defaultValue, Equal(v)) 183 | 184 | 185 | //normal value 186 | v = config.GetStringValue("string", defaultValue) 187 | 188 | Assert(t, "value", Equal(v)) 189 | } 190 | 191 | func TestConfig_GetBoolValue(t *testing.T) { 192 | defaultValue := false 193 | config := GetConfig(defaultNamespace) 194 | 195 | //test default 196 | v := config.GetBoolValue("joe", defaultValue) 197 | 198 | Assert(t, defaultValue, Equal(v)) 199 | 200 | //normal value 201 | v = config.GetBoolValue("bool", defaultValue) 202 | 203 | Assert(t, true, Equal(v)) 204 | 205 | //error type 206 | v = config.GetBoolValue("float", defaultValue) 207 | 208 | Assert(t, defaultValue, Equal(v)) 209 | } 210 | 211 | func TestConfig_GetFloatValue(t *testing.T) { 212 | defaultValue := 100000.1 213 | config := GetConfig(defaultNamespace) 214 | 215 | //test default 216 | v := config.GetFloatValue("joe", defaultValue) 217 | 218 | Assert(t, defaultValue, Equal(v)) 219 | 220 | //normal value 221 | v = config.GetFloatValue("float", defaultValue) 222 | 223 | Assert(t, 190.3, Equal(v)) 224 | 225 | //error type 226 | v = config.GetFloatValue("int", defaultValue) 227 | 228 | Assert(t, float64(1), Equal(v)) 229 | } 230 | 231 | func TestConfig_GetIntValue(t *testing.T) { 232 | defaultValue := 100000 233 | config := GetConfig(defaultNamespace) 234 | 235 | //test default 236 | v := config.GetIntValue("joe", defaultValue) 237 | 238 | Assert(t, defaultValue, Equal(v)) 239 | 240 | //normal value 241 | v = config.GetIntValue("int", defaultValue) 242 | 243 | Assert(t, 1, Equal(v)) 244 | 245 | //error type 246 | v = config.GetIntValue("float", defaultValue) 247 | 248 | Assert(t, defaultValue, Equal(v)) 249 | } -------------------------------------------------------------------------------- /componet_notify.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | const ( 10 | default_notification_id = -1 11 | ) 12 | 13 | var ( 14 | allNotifications *notificationsMap 15 | ) 16 | 17 | type notification struct { 18 | NamespaceName string `json:"namespaceName"` 19 | NotificationId int64 `json:"notificationId"` 20 | } 21 | 22 | type notificationsMap struct { 23 | notifications map[string]int64 24 | sync.RWMutex 25 | } 26 | 27 | type apolloNotify struct { 28 | NotificationId int64 `json:"notificationId"` 29 | NamespaceName string `json:"namespaceName"` 30 | } 31 | 32 | func (n *notificationsMap) setNotify(namespaceName string, notificationId int64) { 33 | n.Lock() 34 | defer n.Unlock() 35 | n.notifications[namespaceName] = notificationId 36 | } 37 | 38 | func (n *notificationsMap) getNotify(namespace string) int64 { 39 | n.RLock() 40 | defer n.RUnlock() 41 | return n.notifications[namespace] 42 | } 43 | 44 | func (n *notificationsMap) getNotifies(namespace string) string { 45 | n.RLock() 46 | defer n.RUnlock() 47 | 48 | notificationArr := make([]*notification, 0) 49 | if namespace == "" { 50 | for namespaceName, notificationId := range n.notifications { 51 | notificationArr = append(notificationArr, 52 | ¬ification{ 53 | NamespaceName: namespaceName, 54 | NotificationId: notificationId, 55 | }) 56 | } 57 | } else { 58 | n := n.notifications[namespace] 59 | notificationArr = append(notificationArr, 60 | ¬ification{ 61 | NamespaceName: namespace, 62 | NotificationId: n, 63 | }) 64 | } 65 | 66 | j, err := json.Marshal(notificationArr) 67 | 68 | if err != nil { 69 | return "" 70 | } 71 | 72 | return string(j) 73 | } 74 | 75 | func initAllNotifications() { 76 | appConfig := GetAppConfig(nil) 77 | 78 | if appConfig == nil { 79 | return 80 | } 81 | initNamespaceNotifications(appConfig.NamespaceName) 82 | } 83 | 84 | func initNamespaceNotifications(namespace string) { 85 | if namespace == empty { 86 | return 87 | } 88 | namespaces := splitNamespaces(namespace, 89 | func(namespace string) {}) 90 | 91 | allNotifications = ¬ificationsMap{ 92 | notifications: namespaces, 93 | } 94 | } 95 | 96 | type NotifyConfigComponent struct { 97 | } 98 | 99 | func (this *NotifyConfigComponent) Start() { 100 | t2 := time.NewTimer(long_poll_interval) 101 | //long poll for sync 102 | for { 103 | select { 104 | case <-t2.C: 105 | notifySyncConfigServices() 106 | t2.Reset(long_poll_interval) 107 | } 108 | } 109 | } 110 | 111 | func notifySyncConfigServices() error { 112 | 113 | remoteConfigs, err := notifyRemoteConfig(nil, empty) 114 | 115 | if err != nil || len(remoteConfigs) == 0 { 116 | return err 117 | } 118 | 119 | updateAllNotifications(remoteConfigs) 120 | 121 | //sync all config 122 | err = autoSyncConfigServices(nil) 123 | 124 | //first sync fail then load config file 125 | if err != nil { 126 | splitNamespaces(appConfig.NamespaceName, func(namespace string) { 127 | config, _ := loadConfigFile(appConfig.BackupConfigPath, namespace) 128 | if config != nil { 129 | updateApolloConfig(config, false) 130 | } 131 | }) 132 | } 133 | //sync all config 134 | return nil 135 | } 136 | 137 | func notifySimpleSyncConfigServices(namespace string) error { 138 | 139 | remoteConfigs, err := notifyRemoteConfig(nil, namespace) 140 | 141 | if err != nil || len(remoteConfigs) == 0 { 142 | return err 143 | } 144 | 145 | updateAllNotifications(remoteConfigs) 146 | 147 | //sync all config 148 | notifications := make(map[string]int64) 149 | notifications[remoteConfigs[0].NamespaceName] = remoteConfigs[0].NotificationId 150 | 151 | return autoSyncNamespaceConfigServices(nil, notifications) 152 | } 153 | 154 | func toApolloConfig(resBody []byte) ([]*apolloNotify, error) { 155 | remoteConfig := make([]*apolloNotify, 0) 156 | 157 | err := json.Unmarshal(resBody, &remoteConfig) 158 | 159 | if err != nil { 160 | logger.Error("Unmarshal Msg Fail,Error:", err) 161 | return nil, err 162 | } 163 | return remoteConfig, nil 164 | } 165 | 166 | func notifyRemoteConfig(newAppConfig *AppConfig, namespace string) ([]*apolloNotify, error) { 167 | appConfig := GetAppConfig(newAppConfig) 168 | if appConfig == nil { 169 | panic("can not find apollo config!please confirm!") 170 | } 171 | urlSuffix := getNotifyUrlSuffix(allNotifications.getNotifies(namespace), appConfig, newAppConfig) 172 | 173 | //seelog.Debugf("allNotifications.getNotifies():%s",allNotifications.getNotifies()) 174 | 175 | notifies, err := requestRecovery(appConfig, &ConnectConfig{ 176 | Uri: urlSuffix, 177 | Timeout: nofity_connect_timeout, 178 | }, &CallBack{ 179 | SuccessCallBack: func(responseBody []byte) (interface{}, error) { 180 | return toApolloConfig(responseBody) 181 | }, 182 | NotModifyCallBack: touchApolloConfigCache, 183 | }) 184 | 185 | if notifies == nil { 186 | return nil, err 187 | } 188 | 189 | return notifies.([]*apolloNotify), err 190 | } 191 | 192 | func updateAllNotifications(remoteConfigs []*apolloNotify) { 193 | for _, remoteConfig := range remoteConfigs { 194 | if remoteConfig.NamespaceName == "" { 195 | continue 196 | } 197 | if allNotifications.getNotify(remoteConfig.NamespaceName) == 0 { 198 | continue 199 | } 200 | 201 | allNotifications.setNotify(remoteConfig.NamespaceName, remoteConfig.NotificationId) 202 | } 203 | } 204 | 205 | func autoSyncConfigServicesSuccessCallBack(responseBody []byte) (o interface{}, err error) { 206 | apolloConfig, err := createApolloConfigWithJson(responseBody) 207 | 208 | if err != nil { 209 | logger.Error("Unmarshal Msg Fail,Error:", err) 210 | return nil, err 211 | } 212 | 213 | updateApolloConfig(apolloConfig, true) 214 | 215 | return nil, nil 216 | } 217 | 218 | func autoSyncConfigServices(newAppConfig *AppConfig) error { 219 | return autoSyncNamespaceConfigServices(newAppConfig, allNotifications.notifications) 220 | } 221 | 222 | func autoSyncNamespaceConfigServices(newAppConfig *AppConfig, notifications map[string]int64) error { 223 | appConfig := GetAppConfig(newAppConfig) 224 | if appConfig == nil { 225 | panic("can not find apollo config!please confirm!") 226 | } 227 | 228 | var err error 229 | for namespace := range notifications { 230 | urlSuffix := getConfigURLSuffix(appConfig, namespace) 231 | 232 | _, err = requestRecovery(appConfig, &ConnectConfig{ 233 | Uri: urlSuffix, 234 | }, &CallBack{ 235 | SuccessCallBack: autoSyncConfigServicesSuccessCallBack, 236 | NotModifyCallBack: touchApolloConfigCache, 237 | }) 238 | if err != nil { 239 | return err 240 | } 241 | } 242 | return err 243 | } 244 | -------------------------------------------------------------------------------- /componet_notify_test.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | . "github.com/tevid/gohamcrest" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestSyncConfigServices(t *testing.T) { 12 | notifySyncConfigServices() 13 | } 14 | 15 | func TestGetRemoteConfig(t *testing.T) { 16 | server := runNormalResponse() 17 | newAppConfig := getTestAppConfig() 18 | newAppConfig.Ip = server.URL 19 | 20 | time.Sleep(1 * time.Second) 21 | 22 | count := 1 23 | var remoteConfigs []*apolloNotify 24 | var err error 25 | for { 26 | count++ 27 | remoteConfigs, err = notifyRemoteConfig(newAppConfig,EMPTY) 28 | 29 | //err keep nil 30 | Assert(t, err,NilVal()) 31 | 32 | //if remote config is nil then break 33 | if remoteConfigs != nil && len(remoteConfigs) > 0 { 34 | break 35 | } 36 | } 37 | 38 | Assert(t, count > 1, Equal(true)) 39 | Assert(t, err,NilVal()) 40 | Assert(t, remoteConfigs,NotNilVal()) 41 | Assert(t, 1, Equal(len(remoteConfigs))) 42 | t.Log("remoteConfigs:", remoteConfigs) 43 | t.Log("remoteConfigs size:", len(remoteConfigs)) 44 | 45 | notify := remoteConfigs[0] 46 | 47 | Assert(t, "application", Equal(notify.NamespaceName)) 48 | Assert(t, true, Equal(notify.NotificationId > 0)) 49 | } 50 | 51 | func TestErrorGetRemoteConfig(t *testing.T) { 52 | server := runErrorResponse() 53 | newAppConfig := getTestAppConfig() 54 | newAppConfig.Ip = server.URL 55 | appConfig.Ip=server.URL 56 | 57 | time.Sleep(1 * time.Second) 58 | 59 | var remoteConfigs []*apolloNotify 60 | var err error 61 | remoteConfigs, err = notifyRemoteConfig(nil,EMPTY) 62 | 63 | Assert(t, err,NotNilVal()) 64 | Assert(t, remoteConfigs,NilVal()) 65 | Assert(t, 0, Equal(len(remoteConfigs))) 66 | t.Log("remoteConfigs:", remoteConfigs) 67 | t.Log("remoteConfigs size:", len(remoteConfigs)) 68 | 69 | Assert(t, "Over Max Retry Still Error!", Equal(err.Error())) 70 | } 71 | 72 | func initNotifications() { 73 | allNotifications = ¬ificationsMap{ 74 | notifications: make(map[string]int64, 1), 75 | } 76 | allNotifications.notifications["application"]=-1 77 | allNotifications.notifications["abc1"]=-1 78 | } 79 | 80 | func TestUpdateAllNotifications(t *testing.T) { 81 | //clear 82 | initNotifications() 83 | 84 | notifyJson := `[ 85 | { 86 | "namespaceName": "application", 87 | "notificationId": 101 88 | } 89 | ]` 90 | notifies := make([]*apolloNotify, 0) 91 | 92 | err := json.Unmarshal([]byte(notifyJson), ¬ifies) 93 | 94 | Assert(t, err,NilVal()) 95 | Assert(t, true, Equal(len(notifies) > 0)) 96 | 97 | updateAllNotifications(notifies) 98 | 99 | Assert(t, true, Equal(len(allNotifications.notifications) > 0)) 100 | Assert(t, int64(101), Equal(allNotifications.notifications["application"])) 101 | } 102 | 103 | func TestUpdateAllNotificationsError(t *testing.T) { 104 | //clear 105 | allNotifications = ¬ificationsMap{ 106 | notifications: make(map[string]int64, 1), 107 | } 108 | 109 | notifyJson := `ffffff` 110 | notifies := make([]*apolloNotify, 0) 111 | 112 | err := json.Unmarshal([]byte(notifyJson), ¬ifies) 113 | 114 | Assert(t, err,NotNilVal()) 115 | Assert(t, true, Equal(len(notifies) == 0)) 116 | 117 | updateAllNotifications(notifies) 118 | 119 | Assert(t, true, Equal(len(allNotifications.notifications) == 0)) 120 | } 121 | 122 | func TestToApolloConfigError(t *testing.T) { 123 | 124 | notified, err := toApolloConfig([]byte("jaskldfjaskl")) 125 | Assert(t, notified,NilVal()) 126 | Assert(t, err,NotNilVal()) 127 | } 128 | 129 | func TestAutoSyncConfigServices(t *testing.T) { 130 | initNotifications() 131 | server := runNormalConfigResponse() 132 | newAppConfig := getTestAppConfig() 133 | newAppConfig.Ip = server.URL 134 | 135 | time.Sleep(1 * time.Second) 136 | 137 | appConfig.NextTryConnTime = 0 138 | 139 | err := autoSyncConfigServices(newAppConfig) 140 | err = autoSyncConfigServices(newAppConfig) 141 | 142 | Assert(t, err,NilVal()) 143 | 144 | config := GetCurrentApolloConfig()[newAppConfig.NamespaceName] 145 | 146 | Assert(t, "100004458", Equal(config.AppId)) 147 | Assert(t, "default", Equal(config.Cluster)) 148 | Assert(t, "application", Equal(config.NamespaceName)) 149 | Assert(t, "20170430092936-dee2d58e74515ff3", Equal(config.ReleaseKey)) 150 | //Assert(t,"value1",config.Configurations["key1"]) 151 | //Assert(t,"value2",config.Configurations["key2"]) 152 | } 153 | 154 | func TestAutoSyncConfigServicesNormal2NotModified(t *testing.T) { 155 | server := runLongNotmodifiedConfigResponse() 156 | newAppConfig := getTestAppConfig() 157 | newAppConfig.Ip = server.URL 158 | time.Sleep(1 * time.Second) 159 | 160 | appConfig.NextTryConnTime = 0 161 | 162 | autoSyncConfigServicesSuccessCallBack([]byte(configResponseStr)) 163 | 164 | config := GetCurrentApolloConfig()[newAppConfig.NamespaceName] 165 | 166 | fmt.Println("sleeping 10s") 167 | 168 | time.Sleep(10 * time.Second) 169 | 170 | fmt.Println("checking agcache time left") 171 | defaultConfigCache := getDefaultConfigCache() 172 | 173 | defaultConfigCache.Range(func(key, value interface{}) bool { 174 | Assert(t, string(value.([]byte)),NotNilVal()) 175 | return true 176 | }) 177 | 178 | Assert(t, "100004458", Equal(config.AppId)) 179 | Assert(t, "default", Equal(config.Cluster)) 180 | Assert(t, "application", Equal(config.NamespaceName)) 181 | Assert(t, "20170430092936-dee2d58e74515ff3", Equal(config.ReleaseKey)) 182 | Assert(t, "value1", Equal(getValue("key1"))) 183 | Assert(t, "value2", Equal(getValue("key2"))) 184 | 185 | err := autoSyncConfigServices(newAppConfig) 186 | 187 | fmt.Println("checking agcache time left") 188 | defaultConfigCache.Range(func(key, value interface{}) bool { 189 | Assert(t, string(value.([]byte)),NotNilVal()) 190 | return true 191 | }) 192 | 193 | fmt.Println(err) 194 | 195 | //sleep for async 196 | time.Sleep(1 * time.Second) 197 | checkBackupFile(t) 198 | } 199 | 200 | func checkBackupFile(t *testing.T) { 201 | newConfig, e := loadConfigFile(appConfig.getBackupConfigPath(),"application") 202 | t.Log(newConfig.Configurations) 203 | Assert(t,e,NilVal()) 204 | Assert(t,newConfig.Configurations,NotNilVal()) 205 | for k, v := range newConfig.Configurations { 206 | Assert(t, getValue(k), Equal(v)) 207 | } 208 | } 209 | 210 | func TestAutoSyncConfigServicesError(t *testing.T) { 211 | //reload app properties 212 | go initFileConfig() 213 | server := runErrorConfigResponse() 214 | newAppConfig := getTestAppConfig() 215 | newAppConfig.Ip = server.URL 216 | 217 | time.Sleep(1 * time.Second) 218 | 219 | err := autoSyncConfigServices(nil) 220 | 221 | Assert(t, err,NotNilVal()) 222 | 223 | config := GetCurrentApolloConfig()[newAppConfig.NamespaceName] 224 | 225 | //still properties config 226 | Assert(t, "test", Equal(config.AppId)) 227 | Assert(t, "dev", Equal(config.Cluster)) 228 | Assert(t, "application", Equal(config.NamespaceName)) 229 | Assert(t, "", Equal(config.ReleaseKey)) 230 | } 231 | -------------------------------------------------------------------------------- /app_config.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | const ( 14 | APP_CONFIG_FILE_NAME = "app.properties" 15 | ENV_CONFIG_FILE_PATH = "AGOLLO_CONF" 16 | ) 17 | 18 | var ( 19 | long_poll_interval = 2 * time.Second //2s 20 | long_poll_connect_timeout = 1 * time.Minute //1m 21 | 22 | connect_timeout = 1 * time.Second //1s 23 | //notify timeout 24 | nofity_connect_timeout = 10 * time.Minute //10m 25 | //for on error retry 26 | on_error_retry_interval = 1 * time.Second //1s 27 | //for typed config agcache of parser result, e.g. integer, double, long, etc. 28 | //max_config_cache_size = 500 //500 agcache key 29 | //config_cache_expire_time = 1 * time.Minute //1 minute 30 | 31 | //max retries connect apollo 32 | max_retries = 5 33 | 34 | //refresh ip list 35 | refresh_ip_list_interval = 20 * time.Minute //20m 36 | 37 | //appconfig 38 | appConfig *AppConfig 39 | 40 | //real servers ip 41 | servers sync.Map 42 | 43 | //next try connect period - 60 second 44 | next_try_connect_period int64 = 60 45 | ) 46 | 47 | type AppConfig struct { 48 | AppId string `json:"appId"` 49 | Cluster string `json:"cluster"` 50 | NamespaceName string `json:"namespaceName"` 51 | Ip string `json:"ip"` 52 | NextTryConnTime int64 `json:"-"` 53 | BackupConfigPath string `json:"backupConfigPath"` 54 | } 55 | 56 | func (this *AppConfig) getBackupConfigPath() string { 57 | return this.BackupConfigPath 58 | } 59 | 60 | func (this *AppConfig) getHost() string { 61 | if strings.HasPrefix(this.Ip, "http") { 62 | if !strings.HasSuffix(this.Ip, "/") { 63 | return this.Ip + "/" 64 | } 65 | return this.Ip 66 | } 67 | return "http://" + this.Ip + "/" 68 | } 69 | 70 | //if this connect is fail will set this time 71 | func (this *AppConfig) setNextTryConnTime(nextTryConnectPeriod int64) { 72 | this.NextTryConnTime = time.Now().Unix() + nextTryConnectPeriod 73 | } 74 | 75 | //is connect by ip directly 76 | //false : no 77 | //true : yes 78 | func (this *AppConfig) isConnectDirectly() bool { 79 | if this.NextTryConnTime >= 0 && this.NextTryConnTime > time.Now().Unix() { 80 | return true 81 | } 82 | 83 | return false 84 | } 85 | 86 | func (this *AppConfig) selectHost() string { 87 | if !this.isConnectDirectly() { 88 | return this.getHost() 89 | } 90 | 91 | host := "" 92 | 93 | servers.Range(func(k, v interface{}) bool { 94 | server := v.(*serverInfo) 95 | // if some node has down then select next node 96 | if server.IsDown { 97 | return true 98 | } 99 | host = k.(string) 100 | return false 101 | }) 102 | 103 | return host 104 | } 105 | 106 | func setDownNode(host string) { 107 | if host == "" || appConfig == nil { 108 | return 109 | } 110 | 111 | if host == appConfig.getHost() { 112 | appConfig.setNextTryConnTime(next_try_connect_period) 113 | } 114 | 115 | servers.Range(func(k, v interface{}) bool { 116 | server := v.(*serverInfo) 117 | // if some node has down then select next node 118 | if k.(string) == host { 119 | server.IsDown = true 120 | return false 121 | } 122 | return true 123 | }) 124 | } 125 | 126 | type serverInfo struct { 127 | AppName string `json:"appName"` 128 | InstanceId string `json:"instanceId"` 129 | HomepageUrl string `json:"homepageUrl"` 130 | IsDown bool `json:"-"` 131 | } 132 | 133 | func initFileConfig() { 134 | // default use application.properties 135 | initConfig(nil) 136 | } 137 | 138 | func initConfig(loadAppConfig func() (*AppConfig, error)) { 139 | var err error 140 | //init config file 141 | appConfig, err = getLoadAppConfig(loadAppConfig) 142 | 143 | if err != nil { 144 | return 145 | } 146 | 147 | initApolloConfigCache(appConfig.NamespaceName) 148 | } 149 | 150 | //initApolloConfigCache 根据namespace初始化apollo配置 151 | func initApolloConfigCache(namespace string) { 152 | func(appConfig *AppConfig) { 153 | splitNamespaces(namespace, func(namespace string) { 154 | apolloConfig := &ApolloConfig{} 155 | apolloConfig.init(appConfig, namespace) 156 | 157 | go updateApolloConfig(apolloConfig, false) 158 | }) 159 | }(appConfig) 160 | } 161 | 162 | // set load app config's function 163 | func getLoadAppConfig(loadAppConfig func() (*AppConfig, error)) (*AppConfig, error) { 164 | if loadAppConfig != nil { 165 | return loadAppConfig() 166 | } 167 | configPath := os.Getenv(ENV_CONFIG_FILE_PATH) 168 | if configPath == "" { 169 | configPath = APP_CONFIG_FILE_NAME 170 | } 171 | return loadJsonConfig(configPath) 172 | } 173 | 174 | //set timer for update ip list 175 | //interval : 20m 176 | func initServerIpList() { 177 | syncServerIpList(nil) 178 | logger.Debug("syncServerIpList started") 179 | 180 | t2 := time.NewTimer(refresh_ip_list_interval) 181 | for { 182 | select { 183 | case <-t2.C: 184 | syncServerIpList(nil) 185 | t2.Reset(refresh_ip_list_interval) 186 | } 187 | } 188 | } 189 | 190 | func syncServerIpListSuccessCallBack(responseBody []byte) (o interface{}, err error) { 191 | logger.Debug("get all server info:", string(responseBody)) 192 | 193 | tmpServerInfo := make([]*serverInfo, 0) 194 | 195 | err = json.Unmarshal(responseBody, &tmpServerInfo) 196 | 197 | if err != nil { 198 | logger.Error("Unmarshal json Fail,Error:", err) 199 | return 200 | } 201 | 202 | if len(tmpServerInfo) == 0 { 203 | logger.Info("get no real server!") 204 | return 205 | } 206 | 207 | for _, server := range tmpServerInfo { 208 | if server == nil { 209 | continue 210 | } 211 | servers.Store(server.HomepageUrl, server) 212 | } 213 | return 214 | } 215 | 216 | //sync ip list from server 217 | //then 218 | //1.update agcache 219 | //2.store in disk 220 | func syncServerIpList(newAppConfig *AppConfig) error { 221 | appConfig := GetAppConfig(newAppConfig) 222 | if appConfig == nil { 223 | panic("can not find apollo config!please confirm!") 224 | } 225 | 226 | _, err := request(getServicesConfigUrl(appConfig), &ConnectConfig{}, &CallBack{ 227 | SuccessCallBack: syncServerIpListSuccessCallBack, 228 | }) 229 | 230 | return err 231 | } 232 | 233 | func GetAppConfig(newAppConfig *AppConfig) *AppConfig { 234 | if newAppConfig != nil { 235 | return newAppConfig 236 | } 237 | return appConfig 238 | } 239 | 240 | func getConfigUrl(config *AppConfig) string { 241 | return getConfigUrlByHost(config, config.getHost()) 242 | } 243 | 244 | func getConfigUrlByHost(config *AppConfig, host string) string { 245 | return fmt.Sprintf("%sconfigs/%s/%s/%s?releaseKey=%s&ip=%s", 246 | host, 247 | url.QueryEscape(config.AppId), 248 | url.QueryEscape(config.Cluster), 249 | url.QueryEscape(config.NamespaceName), 250 | url.QueryEscape(getCurrentApolloConfigReleaseKey(config.NamespaceName)), 251 | getInternal()) 252 | } 253 | 254 | func getConfigURLSuffix(config *AppConfig, namespaceName string) string { 255 | if config == nil { 256 | return "" 257 | } 258 | return fmt.Sprintf("configs/%s/%s/%s?releaseKey=%s&ip=%s", 259 | url.QueryEscape(config.AppId), 260 | url.QueryEscape(config.Cluster), 261 | url.QueryEscape(namespaceName), 262 | url.QueryEscape(getCurrentApolloConfigReleaseKey(namespaceName)), 263 | getInternal()) 264 | } 265 | 266 | func getNotifyUrlSuffix(notifications string, config *AppConfig, newConfig *AppConfig) string { 267 | if newConfig != nil { 268 | return "" 269 | } 270 | return fmt.Sprintf("notifications/v2?appId=%s&cluster=%s¬ifications=%s", 271 | url.QueryEscape(config.AppId), 272 | url.QueryEscape(config.Cluster), 273 | url.QueryEscape(notifications)) 274 | } 275 | 276 | func getServicesConfigUrl(config *AppConfig) string { 277 | return fmt.Sprintf("%sservices/config?appId=%s&ip=%s", 278 | config.getHost(), 279 | url.QueryEscape(config.AppId), 280 | getInternal()) 281 | } 282 | -------------------------------------------------------------------------------- /repository.go: -------------------------------------------------------------------------------- 1 | package agollo 2 | 3 | import ( 4 | "github.com/zouyx/agollo/v2/agcache" 5 | "strconv" 6 | "sync" 7 | ) 8 | 9 | //ConfigFileFormat 配置文件类型 10 | type ConfigFileFormat string 11 | 12 | const ( 13 | //Properties 14 | Properties ConfigFileFormat = "properties" 15 | //XML 16 | XML ConfigFileFormat = "xml" 17 | //JSON 18 | JSON ConfigFileFormat = "json" 19 | //YML 20 | YML ConfigFileFormat = "yml" 21 | //YAML 22 | YAML ConfigFileFormat = "yaml" 23 | ) 24 | 25 | const ( 26 | empty = "" 27 | 28 | //1 minute 29 | configCacheExpireTime = 120 30 | 31 | defaultNamespace = "application" 32 | defaultContentKey = "content" 33 | ) 34 | 35 | var ( 36 | currentConnApolloConfig = ¤tApolloConfig{ 37 | configs: make(map[string]*ApolloConnConfig, 1), 38 | } 39 | 40 | //config from apollo 41 | apolloConfigCache = make(map[string]*Config, 0) 42 | 43 | formatParser = make(map[ConfigFileFormat]ContentParser, 0) 44 | defaultFormatParser = &DefaultParser{} 45 | 46 | cacheFactory = &agcache.DefaultCacheFactory{} 47 | ) 48 | 49 | func init() { 50 | formatParser[Properties] = &PropertiesParser{} 51 | } 52 | 53 | func initDefaultConfig() { 54 | initConfigCache(cacheFactory) 55 | } 56 | 57 | //initNamespaceConfig 根据namespace创建缓存 58 | func initNamespaceConfig(namespace string) { 59 | 60 | createNamespaceConfig(cacheFactory, namespace) 61 | 62 | initNamespaceNotifications(namespace) 63 | } 64 | 65 | func initConfigCache(cacheFactory *agcache.DefaultCacheFactory) { 66 | if appConfig == nil { 67 | logger.Warn("Config is nil,can not init agollo.") 68 | return 69 | } 70 | createNamespaceConfig(cacheFactory, appConfig.NamespaceName) 71 | } 72 | 73 | func createNamespaceConfig(cacheFactory *agcache.DefaultCacheFactory, namespace string) { 74 | splitNamespaces(namespace, func(namespace string) { 75 | if apolloConfigCache[namespace] != nil { 76 | return 77 | } 78 | c := &Config{ 79 | namespace: namespace, 80 | cache: cacheFactory.Create(), 81 | } 82 | apolloConfigCache[namespace] = c 83 | }) 84 | } 85 | 86 | type currentApolloConfig struct { 87 | l sync.RWMutex 88 | configs map[string]*ApolloConnConfig 89 | } 90 | 91 | //Config apollo配置项 92 | type Config struct { 93 | namespace string 94 | cache agcache.CacheInterface 95 | } 96 | 97 | //getConfigValue 获取配置值 98 | func (this *Config) getConfigValue(key string) interface{} { 99 | if this.cache == nil { 100 | logger.Errorf("get config value fail!namespace:%s is not exist!", this.namespace) 101 | return empty 102 | } 103 | 104 | value, err := this.cache.Get(key) 105 | if err != nil { 106 | logger.Errorf("get config value fail!key:%s,err:%s", key, err) 107 | return empty 108 | } 109 | 110 | return string(value) 111 | } 112 | 113 | //getValue 获取配置值(string) 114 | func (this *Config) getValue(key string) string { 115 | value := this.getConfigValue(key) 116 | if value == nil { 117 | return empty 118 | } 119 | 120 | return value.(string) 121 | } 122 | 123 | //GetStringValue 获取配置值(string),获取不到则取默认值 124 | func (this *Config) GetStringValue(key string, defaultValue string) string { 125 | value := this.getValue(key) 126 | if value == empty { 127 | return defaultValue 128 | } 129 | 130 | return value 131 | } 132 | 133 | //GetIntValue 获取配置值(int),获取不到则取默认值 134 | func (this *Config) GetIntValue(key string, defaultValue int) int { 135 | value := this.getValue(key) 136 | 137 | i, err := strconv.Atoi(value) 138 | if err != nil { 139 | logger.Debug("convert to int fail!error:", err) 140 | return defaultValue 141 | } 142 | 143 | return i 144 | } 145 | 146 | //GetFloatValue 获取配置值(float),获取不到则取默认值 147 | func (this *Config) GetFloatValue(key string, defaultValue float64) float64 { 148 | value := this.getValue(key) 149 | 150 | i, err := strconv.ParseFloat(value, 64) 151 | if err != nil { 152 | logger.Debug("convert to float fail!error:", err) 153 | return defaultValue 154 | } 155 | 156 | return i 157 | } 158 | 159 | //GetBoolValue 获取配置值(bool),获取不到则取默认值 160 | func (this *Config) GetBoolValue(key string, defaultValue bool) bool { 161 | value := this.getValue(key) 162 | 163 | b, err := strconv.ParseBool(value) 164 | if err != nil { 165 | logger.Debug("convert to bool fail!error:", err) 166 | return defaultValue 167 | } 168 | 169 | return b 170 | } 171 | 172 | //GetConfig 根据namespace获取apollo配置 173 | func GetConfig(namespace string) *Config { 174 | return GetConfigAndInit(namespace) 175 | } 176 | 177 | //GetConfigAndInit 根据namespace获取apollo配置 178 | func GetConfigAndInit(namespace string) *Config { 179 | if apolloConfigCache[namespace] == nil { 180 | initNamespaceConfig(namespace) 181 | 182 | notifySimpleSyncConfigServices(namespace) 183 | } 184 | return apolloConfigCache[namespace] 185 | } 186 | 187 | //GetConfigCache 根据namespace获取apollo配置的缓存 188 | func GetConfigCache(namespace string) agcache.CacheInterface { 189 | config := GetConfigAndInit(namespace) 190 | if config == nil { 191 | return nil 192 | } 193 | return config.cache 194 | } 195 | 196 | func getDefaultConfigCache() agcache.CacheInterface { 197 | config := GetConfigAndInit(defaultNamespace) 198 | if config != nil { 199 | return config.cache 200 | } 201 | return nil 202 | } 203 | 204 | func updateApolloConfig(apolloConfig *ApolloConfig, isBackupConfig bool) { 205 | if apolloConfig == nil { 206 | logger.Error("apolloConfig is null,can't update!") 207 | return 208 | } 209 | //get change list 210 | changeList := updateApolloConfigCache(apolloConfig.Configurations, configCacheExpireTime, apolloConfig.NamespaceName) 211 | 212 | if len(changeList) > 0 { 213 | //create config change event base on change list 214 | event := createConfigChangeEvent(changeList, apolloConfig.NamespaceName) 215 | 216 | //push change event to channel 217 | pushChangeEvent(event) 218 | } 219 | 220 | //update apollo connection config 221 | currentConnApolloConfig.l.Lock() 222 | defer currentConnApolloConfig.l.Unlock() 223 | 224 | currentConnApolloConfig.configs[apolloConfig.NamespaceName] = &apolloConfig.ApolloConnConfig 225 | 226 | if isBackupConfig { 227 | //write config file async 228 | go writeConfigFile(apolloConfig, appConfig.getBackupConfigPath()) 229 | } 230 | } 231 | 232 | func updateApolloConfigCache(configurations map[string]string, expireTime int, namespace string) map[string]*ConfigChange { 233 | config := GetConfig(namespace) 234 | if config == nil { 235 | return nil 236 | } 237 | 238 | if (configurations == nil || len(configurations) == 0) && config.cache.EntryCount() == 0 { 239 | return nil 240 | } 241 | 242 | //get old keys 243 | mp := map[string]bool{} 244 | config.cache.Range(func(key, value interface{}) bool { 245 | mp[key.(string)] = true 246 | return true 247 | }) 248 | 249 | changes := make(map[string]*ConfigChange) 250 | 251 | if configurations != nil { 252 | // update new 253 | // keys 254 | for key, value := range configurations { 255 | //key state insert or update 256 | //insert 257 | if !mp[key] { 258 | changes[key] = createAddConfigChange(value) 259 | } else { 260 | //update 261 | oldValue, _ := config.cache.Get(key) 262 | if string(oldValue) != value { 263 | changes[key] = createModifyConfigChange(string(oldValue), value) 264 | } 265 | } 266 | 267 | config.cache.Set(key, []byte(value), expireTime) 268 | delete(mp, string(key)) 269 | } 270 | } 271 | 272 | // remove del keys 273 | for key := range mp { 274 | //get old value and del 275 | oldValue, _ := config.cache.Get(key) 276 | changes[key] = createDeletedConfigChange(string(oldValue)) 277 | 278 | config.cache.Del(key) 279 | } 280 | 281 | return changes 282 | } 283 | 284 | //base on changeList create Change event 285 | func createConfigChangeEvent(changes map[string]*ConfigChange, nameSpace string) *ChangeEvent { 286 | return &ChangeEvent{ 287 | Namespace: nameSpace, 288 | Changes: changes, 289 | } 290 | } 291 | 292 | func touchApolloConfigCache() error { 293 | return nil 294 | } 295 | 296 | //GetApolloConfigCache 获取默认namespace的apollo配置 297 | func GetApolloConfigCache() agcache.CacheInterface { 298 | return getDefaultConfigCache() 299 | } 300 | 301 | //GetCurrentApolloConfig 获取Apollo链接配置 302 | func GetCurrentApolloConfig() map[string]*ApolloConnConfig { 303 | currentConnApolloConfig.l.RLock() 304 | defer currentConnApolloConfig.l.RUnlock() 305 | 306 | return currentConnApolloConfig.configs 307 | } 308 | 309 | func getCurrentApolloConfigReleaseKey(namespace string) string { 310 | currentConnApolloConfig.l.RLock() 311 | defer currentConnApolloConfig.l.RUnlock() 312 | config := currentConnApolloConfig.configs[namespace] 313 | if config == nil { 314 | return empty 315 | } 316 | 317 | return config.ReleaseKey 318 | } 319 | 320 | func getConfigValue(key string) interface{} { 321 | value, err := getDefaultConfigCache().Get(key) 322 | if err != nil { 323 | logger.Errorf("get config value fail!key:%s,err:%s", key, err) 324 | return empty 325 | } 326 | 327 | return string(value) 328 | } 329 | 330 | func getValue(key string) string { 331 | value := getConfigValue(key) 332 | if value == nil { 333 | return empty 334 | } 335 | 336 | return value.(string) 337 | } 338 | 339 | func GetStringValue(key string, defaultValue string) string { 340 | value := getValue(key) 341 | if value == empty { 342 | return defaultValue 343 | } 344 | 345 | return value 346 | } 347 | 348 | func GetIntValue(key string, defaultValue int) int { 349 | value := getValue(key) 350 | 351 | i, err := strconv.Atoi(value) 352 | if err != nil { 353 | logger.Debug("convert to int fail!error:", err) 354 | return defaultValue 355 | } 356 | 357 | return i 358 | } 359 | 360 | func GetFloatValue(key string, defaultValue float64) float64 { 361 | value := getValue(key) 362 | 363 | i, err := strconv.ParseFloat(value, 64) 364 | if err != nil { 365 | logger.Debug("convert to float fail!error:", err) 366 | return defaultValue 367 | } 368 | 369 | return i 370 | } 371 | 372 | func GetBoolValue(key string, defaultValue bool) bool { 373 | value := getValue(key) 374 | 375 | b, err := strconv.ParseBool(value) 376 | if err != nil { 377 | logger.Debug("convert to bool fail!error:", err) 378 | return defaultValue 379 | } 380 | 381 | return b 382 | } 383 | 384 | //GetContent 获取配置文件内容 385 | func (c *Config) GetContent(format ConfigFileFormat) string { 386 | parser := formatParser[format] 387 | if parser == nil { 388 | parser = defaultFormatParser 389 | } 390 | s, err := parser.parse(c.cache) 391 | if err != nil { 392 | logger.Debug("GetContent fail ! error:", err) 393 | } 394 | return s 395 | } 396 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------