diff --git a/.github/workflows/module_controller_ci_build_batch_deploy_to_aliyun.yml b/.github/workflows/module_controller_ci_build_batch_deploy_to_aliyun.yml index 2fb63e0ae..8f0cb7d87 100644 --- a/.github/workflows/module_controller_ci_build_batch_deploy_to_aliyun.yml +++ b/.github/workflows/module_controller_ci_build_batch_deploy_to_aliyun.yml @@ -167,7 +167,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -183,11 +182,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "0" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":1}}' --type=merge exit 0 else echo "等待字段值满足条件..." @@ -213,7 +212,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -229,11 +227,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "1" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":2}}' --type=merge exit 0 else echo "等待字段值满足条件..." @@ -353,7 +351,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -369,11 +366,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "0" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":1}}' --type=merge exit 0 else echo "等待字段值满足条件..." @@ -400,7 +397,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -416,11 +412,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "1" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":2}}' --type=merge exit 0 else echo "等待字段值满足条件..." @@ -496,7 +492,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -512,11 +507,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "0" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":1}}' --type=merge exit 0 else echo "等待字段值满足条件..." @@ -579,7 +574,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -595,11 +589,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "1" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":2}}' --type=merge exit 0 else echo "等待字段值满足条件..." diff --git a/.github/workflows/module_controller_ci_build_batch_scaleup_then_scaledown_deploy_to_aliyun.yml b/.github/workflows/module_controller_ci_build_batch_scaleup_then_scaledown_deploy_to_aliyun.yml index c8cc1b711..7dfad5f87 100644 --- a/.github/workflows/module_controller_ci_build_batch_scaleup_then_scaledown_deploy_to_aliyun.yml +++ b/.github/workflows/module_controller_ci_build_batch_scaleup_then_scaledown_deploy_to_aliyun.yml @@ -167,7 +167,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -183,11 +182,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "0" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":1}}' --type=merge exit 0 else echo "等待字段值满足条件..." @@ -579,7 +578,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -595,11 +593,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "0" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":1}}' --type=merge exit 0 else echo "等待字段值满足条件..." @@ -738,7 +736,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -754,11 +751,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "0" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":1}}' --type=merge exit 0 else echo "等待字段值满足条件..." diff --git a/.github/workflows/module_controller_ci_build_batch_symmetric_deploy_to_aliyun.yml b/.github/workflows/module_controller_ci_build_batch_symmetric_deploy_to_aliyun.yml index 3a831315f..af3f184f3 100644 --- a/.github/workflows/module_controller_ci_build_batch_symmetric_deploy_to_aliyun.yml +++ b/.github/workflows/module_controller_ci_build_batch_symmetric_deploy_to_aliyun.yml @@ -167,7 +167,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -183,11 +182,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "0" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":1}}' --type=merge exit 0 else echo "等待字段值满足条件..." @@ -213,7 +212,6 @@ jobs: run: | # 定义要等待的资源名称和字段值 moduledeploymentname=$(kubectl get moduledeployment -o name) - desired_field_value=true # 定义等待的超时时间(以秒为单位) timeout_seconds=300 @@ -229,11 +227,11 @@ jobs: fi # 使用 kubectl get 命令获取资源对象的详细信息,并提取自定义字段的值 - field_value=$(kubectl get $moduledeploymentname -o custom-columns=PAUSE:.spec.pause --no-headers) + field_value=$(kubectl get $moduledeploymentname -o custom-columns=CONFIRMBATCHNUM:.spec.confirmBatchNum --no-headers) - if [ "$field_value" == "$desired_field_value" ]; then + if [ "$field_value" == "1" ]; then echo "字段值已满足条件,执行分组确认" - kubectl patch $moduledeploymentname -p '{"spec":{"pause":false}}' --type=merge + kubectl patch $moduledeploymentname -p '{"spec":{"confirmBatchNum":2}}' --type=merge exit 0 else echo "等待字段值满足条件..." diff --git a/docs/content/zh-cn/docs/introduction/architecture/arch-principle.md b/docs/content/zh-cn/docs/introduction/architecture/arch-principle.md index 4159564f9..fbcb3f03d 100644 --- a/docs/content/zh-cn/docs/introduction/architecture/arch-principle.md +++ b/docs/content/zh-cn/docs/introduction/architecture/arch-principle.md @@ -49,7 +49,8 @@ SOFAArk 的隔离方式和 OSGI 是一致的,但是在共享方面 OSGI 和 JP ![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2023/png/149473/1695175141130-d3b55e17-70c3-4e7c-aeef-2e071f89ada8.png#clientId=uaaa65411-0843-4&from=paste&height=316&id=u589ef06e&originHeight=632&originWidth=3642&originalType=binary&ratio=2&rotation=0&showTitle=false&size=139102&status=done&style=none&taskId=uf9f96d68-7456-4af5-951e-d9351092988&title=&width=1821)
图中的箭头是双向的,如果当前微服务拆分过多,也可以将多个微服务低成本改造成模块合并部署在一个 JVM 内。所以这里的本质是通过在单体架构和微服务架构之间增加一个可以双向过渡的模块化架构,降低改造成本的同时,也让开发者可以根据业务发展按需演进或回退。这样可以把微服务的这几个问题解决掉 ### 模块化架构的优势 -模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2023/png/149473/1695180240688-82520d0c-2304-47dc-a9f3-22af08424100.png#clientId=ueb39d37f-ca7b-4&from=paste&height=237&id=u4c60feb3&originHeight=688&originWidth=1504&originalType=binary&ratio=2&rotation=0&showTitle=false&size=437668&status=done&style=none&taskId=uf04ead3d-7cf7-41e7-bfff-81857bf5918&title=&width=518) +模块化架构的优势主要集中在这四点:快、省、灵活部署、可演进,
![image.png](https://github.com/sofastack/sofa-serverless/assets/3754074/11d1d662-d33b-482b-946b-bf600aeb34da) + 与传统应用对比数据如下,可以看到在研发阶段、部署阶段、运行阶段都得到了10倍以上的提升效果。
![image.png](https://intranetproxy.alipay.com/skylark/lark/0/2023/png/149473/1695180250909-f5eca1b3-c416-4bac-9732-549a9bed8b87.png#clientId=ueb39d37f-ca7b-4&from=paste&height=261&id=u8907b613&originHeight=522&originWidth=2838&originalType=binary&ratio=2&rotation=0&showTitle=false&size=219589&status=done&style=none&taskId=ua4b2bd1b-a75f-4945-abce-68826a43377&title=&width=1419) diff --git a/docs/content/zh-cn/docs/tutorials/base-create/springboot-and-sofaboot.md b/docs/content/zh-cn/docs/tutorials/base-create/springboot-and-sofaboot.md index c7611984f..5e3798fb8 100644 --- a/docs/content/zh-cn/docs/tutorials/base-create/springboot-and-sofaboot.md +++ b/docs/content/zh-cn/docs/tutorials/base-create/springboot-and-sofaboot.md @@ -20,7 +20,7 @@ spring.application.name = ${替换为实际基座应用名} #### 修改主 pom.xml ```xml - 2.2.6 + 2.2.7 0.5.6 ``` diff --git a/module-controller/api/v1alpha1/moduledeployment_types.go b/module-controller/api/v1alpha1/moduledeployment_types.go index f47b0237f..afe5e91e4 100644 --- a/module-controller/api/v1alpha1/moduledeployment_types.go +++ b/module-controller/api/v1alpha1/moduledeployment_types.go @@ -163,10 +163,9 @@ type ModuleDeploymentSpec struct { ProgressDeadlineSeconds int32 `json:"progressDeadlineSeconds,omitempty"` - // Indicates that the moduleDeployment is paused and will not be processed by the - // moduleDeployment controller. - // +optional - Pause bool `json:"pause,omitempty"` + // +kubebuilder:default:=0 + // +kubebuilder:validation:Minimum=0 + ConfirmBatchNum int32 `json:"confirmBatchNum,omitempty"` OperationStrategy ModuleOperationStrategy `json:"operationStrategy,omitempty"` diff --git a/module-controller/config/crd/bases/serverless.alipay.com_moduledeployments.yaml b/module-controller/config/crd/bases/serverless.alipay.com_moduledeployments.yaml index 1fe788c00..78f4e4af9 100644 --- a/module-controller/config/crd/bases/serverless.alipay.com_moduledeployments.yaml +++ b/module-controller/config/crd/bases/serverless.alipay.com_moduledeployments.yaml @@ -41,6 +41,11 @@ spec: Important: Run "make" to regenerate code after modifying this file' minLength: 1 type: string + confirmBatchNum: + default: 0 + format: int32 + minimum: 0 + type: integer minReadySeconds: format: int32 type: integer @@ -75,10 +80,6 @@ spec: useBeta: type: boolean type: object - pause: - description: Indicates that the moduleDeployment is paused and will - not be processed by the moduleDeployment controller. - type: boolean progressDeadlineSeconds: format: int32 type: integer diff --git a/module-controller/internal/controller/moduledeployment_controller.go b/module-controller/internal/controller/moduledeployment_controller.go index 9cc913d56..c3b39bf5a 100644 --- a/module-controller/internal/controller/moduledeployment_controller.go +++ b/module-controller/internal/controller/moduledeployment_controller.go @@ -107,7 +107,10 @@ func (r *ModuleDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req event.PublishModuleDeploymentCreateEvent(r.Client, ctx, moduleDeployment) } - if moduleDeployment.Spec.Pause { + if moduleDeployment.Status.ReleaseStatus != nil && + moduleDeployment.Spec.ConfirmBatchNum < moduleDeployment.Status.ReleaseStatus.CurrentBatch && + moduleDeployment.Status.ReleaseStatus.Progress == v1alpha1.ModuleDeploymentReleaseProgressPaused { + return ctrl.Result{}, nil } @@ -162,19 +165,26 @@ func (r *ModuleDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, err } } - case v1alpha1.ModuleDeploymentReleaseProgressWaitingForConfirmation: - moduleDeployment.Spec.Pause = true - if err := r.Update(ctx, moduleDeployment); err != nil { - return ctrl.Result{}, err + + if moduleDeployment.Spec.ConfirmBatchNum > 0 { + moduleDeployment.Spec.ConfirmBatchNum = 0 + if err := utils.UpdateResource(r.Client, ctx, moduleDeployment); err != nil { + return ctrl.Result{}, err + } } + case v1alpha1.ModuleDeploymentReleaseProgressWaitingForConfirmation: + moduleDeployment.Status.ReleaseStatus.Progress = v1alpha1.ModuleDeploymentReleaseProgressPaused log.Log.Info("update moduleDeployment releaseStatus progress to paused", "moduleDeploymentName", moduleDeployment.Name) if err := utils.UpdateStatus(r.Client, ctx, moduleDeployment); err != nil { return ctrl.Result{}, utils.Error(err, "update moduleDeployment releaseStatus progress to paused failed") } case v1alpha1.ModuleDeploymentReleaseProgressPaused: - if !moduleDeployment.Spec.Pause && time.Since(moduleDeployment.Status.ReleaseStatus.NextReconcileTime.Time) >= 0 { + if time.Since(moduleDeployment.Status.ReleaseStatus.NextReconcileTime.Time) >= 0 && + moduleDeployment.Spec.ConfirmBatchNum == moduleDeployment.Status.ReleaseStatus.CurrentBatch { + + moduleDeployment.Status.ReleaseStatus.CurrentBatch += 1 moduleDeployment.Status.ReleaseStatus.Progress = v1alpha1.ModuleDeploymentReleaseProgressExecuting log.Log.Info("update moduleDeployment progress from paused to executing", "moduleDeploymentName", moduleDeployment.Name) if err := utils.UpdateStatus(r.Client, ctx, moduleDeployment); err != nil { @@ -465,7 +475,6 @@ func (r *ModuleDeploymentReconciler) updateModuleReplicaSet(ctx context.Context, } } // TODO update current batch - moduleDeployment.Status.ReleaseStatus.CurrentBatch += 1 moduleDeployment.Status.ReleaseStatus.BatchProgress = v1alpha1.ModuleDeploymentReleaseProgressExecuting log.Log.Info("update moduleDeployment batch progress to executing", "moduleDeploymentName", moduleDeployment.Name) err = utils.UpdateStatus(r.Client, ctx, moduleDeployment) diff --git a/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go b/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go index 4933e6532..e9171b3f1 100644 --- a/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go +++ b/module-controller/internal/controller/moduledeployment_controller_operation_strategy_suit_test.go @@ -420,7 +420,7 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { return err } - if !moduleDeployment.Spec.Pause { + if moduleDeployment.Spec.ConfirmBatchNum != 0 { return fmt.Errorf("the deployment is not paused") } @@ -435,18 +435,22 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { Eventually(func() bool { Expect(k8sClient.Get(context.TODO(), nn, &moduleDeployment)).Should(Succeed()) - moduleDeployment.Spec.Pause = false + moduleDeployment.Spec.ConfirmBatchNum = 1 return Expect(k8sClient.Update(context.TODO(), &moduleDeployment)).Should(Succeed()) }, timeout, interval).Should(BeTrue()) }) + It("wait moduleDeployment Completed", func() { + waitModuleDeploymentCompleted(moduleDeploymentName, namespace) + }) + It("4. check if the moduleDeployment status is completed", func() { Eventually(func() bool { if k8sClient.Get(context.TODO(), nn, &moduleDeployment) != nil { return false } - if moduleDeployment.Spec.Pause != false { + if moduleDeployment.Spec.ConfirmBatchNum != 0 { return false } @@ -470,7 +474,7 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { return err } - if !moduleDeployment.Spec.Pause { + if moduleDeployment.Spec.ConfirmBatchNum != 0 { return fmt.Errorf("the deployment is not paused") } @@ -485,18 +489,22 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { Eventually(func() bool { Expect(k8sClient.Get(context.TODO(), nn, &moduleDeployment)).Should(Succeed()) - moduleDeployment.Spec.Pause = false + moduleDeployment.Spec.ConfirmBatchNum = 1 return Expect(k8sClient.Update(context.TODO(), &moduleDeployment)).Should(Succeed()) }, timeout, interval).Should(BeTrue()) }) + It("wait moduleDeployment Completed", func() { + waitModuleDeploymentTerminated(moduleDeploymentName, namespace) + }) + It("9. check if the moduleDeployment status is Terminated", func() { Eventually(func() error { if err := k8sClient.Get(context.TODO(), nn, &moduleDeployment); err != nil { return err } - if moduleDeployment.Spec.Pause != false { + if moduleDeployment.Spec.ConfirmBatchNum != 1 { return fmt.Errorf("the module-deployment is paused") } @@ -570,7 +578,7 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { return err } - if !moduleDeployment.Spec.Pause { + if moduleDeployment.Spec.ConfirmBatchNum > 0 { return fmt.Errorf("the deployment is not paused") } @@ -585,18 +593,22 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { Eventually(func() bool { Expect(k8sClient.Get(context.TODO(), nn, &moduleDeployment)).Should(Succeed()) - moduleDeployment.Spec.Pause = false + moduleDeployment.Spec.ConfirmBatchNum = 1 return Expect(k8sClient.Update(context.TODO(), &moduleDeployment)).Should(Succeed()) }, timeout, interval).Should(BeTrue()) }) + It("wait moduleDeployment Completed", func() { + waitModuleDeploymentCompleted(moduleDeploymentName, namespace) + }) + It("4. check if the moduleDeployment status is completed", func() { Eventually(func() bool { if k8sClient.Get(context.TODO(), nn, &moduleDeployment) != nil { return false } - if moduleDeployment.Spec.Pause != false { + if moduleDeployment.Spec.ConfirmBatchNum != 0 { return false } @@ -619,7 +631,7 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { return err } - if !moduleDeployment.Spec.Pause { + if moduleDeployment.Spec.ConfirmBatchNum > 0 { return fmt.Errorf("the deployment is not paused") } @@ -634,7 +646,7 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { Eventually(func() bool { Expect(k8sClient.Get(context.TODO(), nn, &moduleDeployment)).Should(Succeed()) - moduleDeployment.Spec.Pause = false + moduleDeployment.Spec.ConfirmBatchNum = 1 return Expect(k8sClient.Update(context.TODO(), &moduleDeployment)).Should(Succeed()) }, timeout, interval).Should(BeTrue()) }) @@ -645,7 +657,7 @@ var _ = Describe("ModuleDeployment Controller OperationStrategy Test", func() { return err } - if moduleDeployment.Spec.Pause != false { + if moduleDeployment.Spec.ConfirmBatchNum != 1 { return fmt.Errorf("the module-deployment is paused") } @@ -834,3 +846,18 @@ func waitModuleDeploymentCompleted(moduleDeploymentName string, namespace string time.Sleep(5 * time.Second) waitModuleDeploymentCompleted(moduleDeploymentName, namespace) } + +func waitModuleDeploymentTerminated(moduleDeploymentName string, namespace string) { + key := types.NamespacedName{ + Name: moduleDeploymentName, + Namespace: namespace, + } + newModuleDeployment := &v1alpha1.ModuleDeployment{} + Expect(k8sClient.Get(context.TODO(), key, newModuleDeployment)).Should(Succeed()) + progress := newModuleDeployment.Status.ReleaseStatus.Progress + if progress == v1alpha1.ModuleDeploymentReleaseProgressTerminated { + return + } + time.Sleep(5 * time.Second) + waitModuleDeploymentTerminated(moduleDeploymentName, namespace) +} diff --git a/samples/dubbo-samples/rpc/dubbo26/pom.xml b/samples/dubbo-samples/rpc/dubbo26/pom.xml index bc52f1810..e0716f3b6 100644 --- a/samples/dubbo-samples/rpc/dubbo26/pom.xml +++ b/samples/dubbo-samples/rpc/dubbo26/pom.xml @@ -19,7 +19,7 @@ 8 2.6.12 2.7.16 - 2.2.6 + 2.2.7 0.5.6 3.4.2 3.8.1 diff --git a/samples/dubbo-samples/rpc/dubbo27/pom.xml b/samples/dubbo-samples/rpc/dubbo27/pom.xml index ec99eae6b..de1d5c841 100644 --- a/samples/dubbo-samples/rpc/dubbo27/pom.xml +++ b/samples/dubbo-samples/rpc/dubbo27/pom.xml @@ -19,7 +19,7 @@ 8 2.7.23 2.7.16 - 2.2.6 + 2.2.7 0.5.6 3.4.2 3.8.1 diff --git a/samples/dubbo-samples/rpc/dubbo3/pom.xml b/samples/dubbo-samples/rpc/dubbo3/pom.xml index 249c00bfa..ca7f54b01 100644 --- a/samples/dubbo-samples/rpc/dubbo3/pom.xml +++ b/samples/dubbo-samples/rpc/dubbo3/pom.xml @@ -21,7 +21,7 @@ UTF-8 UTF-8 2.7.16 - 2.2.6 + 2.2.7 0.5.6 3.4.2 3.8.1 diff --git a/samples/feature-samples/pom.xml b/samples/feature-samples/pom.xml index 2aee02941..4cca52dfe 100644 --- a/samples/feature-samples/pom.xml +++ b/samples/feature-samples/pom.xml @@ -20,7 +20,7 @@ 2.7.16 1.8 - 2.2.6 + 2.2.7 0.5.6 3.4.2 1.7.1 diff --git a/samples/sofaboot-samples/pom.xml b/samples/sofaboot-samples/pom.xml index db5044a1d..0a285bb22 100644 --- a/samples/sofaboot-samples/pom.xml +++ b/samples/sofaboot-samples/pom.xml @@ -21,7 +21,7 @@ 1.8 - 2.2.6 + 2.2.7 0.5.6 2.9.1 1.3.2 diff --git a/samples/springboot-samples/pom.xml b/samples/springboot-samples/pom.xml index 44308104b..60086128c 100644 --- a/samples/springboot-samples/pom.xml +++ b/samples/springboot-samples/pom.xml @@ -20,7 +20,7 @@ 2.7.16 1.8 - 2.2.6 + 2.2.7 0.5.7-SNAPSHOT 3.4.2 1.7.1 diff --git a/sofa-serverless-runtime/pom.xml b/sofa-serverless-runtime/pom.xml index e49243dff..883980ed3 100644 --- a/sofa-serverless-runtime/pom.xml +++ b/sofa-serverless-runtime/pom.xml @@ -10,7 +10,7 @@ 0.5.7-SNAPSHOT - 2.2.6 + 2.2.7 2.7.15 UTF-8 UTF-8 diff --git a/sofa-serverless-runtime/sofa-serverless-common/src/main/java/com/alipay/sofa/serverless/common/util/MultiBizProperties.java b/sofa-serverless-runtime/sofa-serverless-common/src/main/java/com/alipay/sofa/serverless/common/util/MultiBizProperties.java new file mode 100644 index 000000000..3b91211ef --- /dev/null +++ b/sofa-serverless-runtime/sofa-serverless-common/src/main/java/com/alipay/sofa/serverless/common/util/MultiBizProperties.java @@ -0,0 +1,450 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.serverless.common.util; + +import java.io.*; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Support multi-business Properties
+ * Isolating configuration separation between different business modules
+ * The default value of the configuration of the base application is used
+ *

+ * If you want to use, you need to write the code in you base application + *

+ * + * MultiBizProperties.initSystem(); + * + */ +public class MultiBizProperties extends Properties { + + private final String bizClassLoaderName; + + private static final String BIZ_CLASS_LOADER = "com.alipay.sofa.ark.container.service.classloader.BizClassLoader"; + + private Map> modifiedKeysMap = new HashMap<>(); + + private final Properties baseProperties; + private Map bizPropertiesMap; + + private MultiBizProperties(String bizClassLoaderName, Properties baseProperties) { + this.bizPropertiesMap = new HashMap<>(); + this.baseProperties = baseProperties; + this.bizClassLoaderName = bizClassLoaderName; + } + + public MultiBizProperties(String bizClassLoaderName) { + this(bizClassLoaderName, new Properties()); + } + + public synchronized Object setProperty(String key, String value) { + addModifiedKey(key); + return getWriteProperties().setProperty(key, value); + } + + @Override + public String getProperty(String key) { + return getReadProperties().getProperty(key); + } + + @Override + public String getProperty(String key, String defaultValue) { + return getReadProperties().getProperty(key, defaultValue); + } + + @Override + public synchronized void load(Reader reader) throws IOException { + Properties properties = new Properties(); + properties.load(reader); + getWriteProperties().putAll(properties); + addModifiedKeys(properties.stringPropertyNames()); + } + + @Override + public synchronized void load(InputStream inStream) throws IOException { + Properties properties = new Properties(); + properties.load(inStream); + getWriteProperties().putAll(properties); + addModifiedKeys(properties.stringPropertyNames()); + } + + @Override + public void list(PrintStream out) { + getWriteProperties().list(out); + } + + @Override + public void list(PrintWriter out) { + getWriteProperties().list(out); + } + + @Override + public void save(OutputStream out, String comments) { + Properties properties = getWriteProperties(); + properties.save(out, comments); + } + + @Override + public void store(Writer writer, String comments) throws IOException { + Properties properties = getReadProperties(); + properties.store(writer, comments); + } + + @Override + public void store(OutputStream out, String comments) throws IOException { + Properties properties = getReadProperties(); + properties.store(out, comments); + } + + @Override + public synchronized void loadFromXML(InputStream in) throws IOException { + Properties properties = new Properties(); + properties.loadFromXML(in); + getWriteProperties().putAll(properties); + addModifiedKeys(properties.stringPropertyNames()); + } + + @Override + public void storeToXML(OutputStream os, String comment) throws IOException { + Properties properties = getReadProperties(); + properties.storeToXML(os, comment); + } + + @Override + public void storeToXML(OutputStream os, String comment, String encoding) throws IOException { + Properties properties = getReadProperties(); + properties.storeToXML(os, comment, encoding); + } + + @Override + public Enumeration propertyNames() { + return getReadProperties().propertyNames(); + } + + @Override + public Set stringPropertyNames() { + return getReadProperties().stringPropertyNames(); + } + + @Override + public synchronized boolean remove(Object key, Object value) { + boolean success = getWriteProperties().remove(key, value); + if (success) { + addModifiedKey(key.toString()); + } + return success; + } + + @Override + public synchronized Object get(Object key) { + return getReadProperties().get(key); + } + + @Override + public synchronized Object remove(Object key) { + if (key != null) { + addModifiedKey(key.toString()); + } + return getWriteProperties().remove(key); + } + + @Override + public synchronized Object put(Object key, Object value) { + String text = key == null ? null : key.toString(); + addModifiedKey(text); + return getWriteProperties().put(key, value); + } + + @Override + public synchronized boolean equals(Object o) { + return getReadProperties().equals(o); + } + + @Override + public synchronized String toString() { + return getReadProperties().toString(); + } + + @Override + public Collection values() { + return getReadProperties().values(); + } + + @Override + public synchronized int hashCode() { + return getReadProperties().hashCode(); + } + + @Override + public synchronized void clear() { + Set keys = baseProperties.stringPropertyNames(); + getWriteProperties().clear(); + addModifiedKeys(keys); + } + + @Override + public synchronized Object clone() { + MultiBizProperties mbp = new MultiBizProperties(bizClassLoaderName, baseProperties); + mbp.bizPropertiesMap = new HashMap<>(); + bizPropertiesMap.forEach((k, p) -> mbp.bizPropertiesMap.put(k, (Properties) p.clone())); + mbp.bizPropertiesMap.putAll(bizPropertiesMap); + mbp.modifiedKeysMap = new HashMap<>(); + modifiedKeysMap.forEach((k, s) -> mbp.modifiedKeysMap.put(k, new HashSet<>(s))); + return mbp; + } + + @Override + public synchronized boolean replace(Object key, Object oldValue, Object newValue) { + Object curValue = get(key); + if (!Objects.equals(curValue, oldValue) || (curValue == null && !containsKey(key))) { + return false; + } + put(key, newValue); + return true; + } + + @Override + public synchronized boolean isEmpty() { + return getReadProperties().isEmpty(); + } + + @Override + public synchronized Object replace(Object key, Object value) { + Object curValue; + if (((curValue = get(key)) != null) || containsKey(key)) { + curValue = put(key, value); + } + return curValue; + } + + @Override + public synchronized boolean containsKey(Object key) { + return getReadProperties().containsKey(key); + } + + @Override + public synchronized boolean contains(Object value) { + return getReadProperties().contains(value); + } + + @Override + public synchronized void replaceAll(BiFunction function) { + Map map = new HashMap(); + for (Map.Entry entry : entrySet()) { + Object k = entry.getKey(); + Object v = entry.getValue(); + v = function.apply(k, v); + map.put(k, v); + } + putAll(map); + } + + @Override + public synchronized int size() { + return getReadProperties().size(); + } + + @Override + public Set> entrySet() { + return getReadProperties().entrySet(); + } + + @Override + public synchronized void putAll(Map map) { + Set keys = new HashSet<>(); + for (Object key : map.keySet()) { + String text = key == null ? null : key.toString(); + keys.add(text); + } + addModifiedKeys(keys); + getWriteProperties().putAll(map); + } + + @Override + public synchronized Object computeIfAbsent(Object key, + Function mappingFunction) { + Object value = get(key); + if (value == null) { + Object newValue = mappingFunction.apply(key); + if (newValue != null) { + put(key, newValue); + return newValue; + } + } + return value; + } + + @Override + public synchronized Enumeration elements() { + return getReadProperties().elements(); + } + + @Override + public synchronized void forEach(BiConsumer action) { + getReadProperties().forEach(action); + } + + @Override + public synchronized Object putIfAbsent(Object key, Object value) { + Object v = get(key); + if (v == null) { + v = put(key, value); + } + return v; + } + + @Override + public synchronized Enumeration keys() { + return getReadProperties().keys(); + } + + @Override + public Set keySet() { + return getReadProperties().keySet(); + } + + @Override + public boolean containsValue(Object value) { + return getReadProperties().containsValue(value); + } + + @Override + public synchronized Object getOrDefault(Object key, Object defaultValue) { + return getReadProperties().getOrDefault(key, defaultValue); + } + + @Override + public synchronized Object computeIfPresent(Object key, + BiFunction remappingFunction) { + Object oldValue = get(key); + if (oldValue == null) { + return null; + } + Object newValue = remappingFunction.apply(key, oldValue); + if (newValue != null) { + put(key, newValue); + return newValue; + } + remove(key); + return null; + } + + @Override + public synchronized Object compute(Object key, + BiFunction remappingFunction) { + Object oldValue = get(key); + Object newValue = remappingFunction.apply(key, oldValue); + if (newValue == null) { + if (oldValue != null || containsKey(key)) { + remove(key); + } + return null; + } + put(key, newValue); + return newValue; + } + + @Override + public synchronized Object merge(Object key, Object value, + BiFunction remappingFunction) { + Object oldValue = get(key); + Object newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value); + if (newValue == null) { + remove(key); + } else { + put(key, newValue); + } + return newValue; + } + + private synchronized Properties getReadProperties() { + Properties bizProperties = getWriteProperties(); + if (bizProperties == baseProperties) { + return baseProperties; + } + Properties properties = new Properties(); + properties.putAll(baseProperties); + Set modifiedKeys = getModifiedKeys(); + if (modifiedKeys != null) { + modifiedKeys.forEach(properties::remove); + } + properties.putAll(bizProperties); + return properties; + } + + private synchronized Properties getWriteProperties() { + ClassLoader invokeClassLoader = Thread.currentThread().getContextClassLoader(); + if (bizPropertiesMap.containsKey(invokeClassLoader)) { + return bizPropertiesMap.get(invokeClassLoader); + } + for (ClassLoader classLoader = invokeClassLoader; classLoader != null; classLoader = classLoader.getParent()) { + String name = classLoader.getClass().getName(); + if (Objects.equals(name, bizClassLoaderName)) { + Properties props = bizPropertiesMap.computeIfAbsent(classLoader, k -> new Properties()); + bizPropertiesMap.put(invokeClassLoader, props); + return props; + } + } + bizPropertiesMap.put(invokeClassLoader, baseProperties); + return baseProperties; + } + + private synchronized Set getModifiedKeys() { + ClassLoader invokeClassLoader = Thread.currentThread().getContextClassLoader(); + if (modifiedKeysMap.containsKey(invokeClassLoader)) { + return modifiedKeysMap.get(invokeClassLoader); + } + for (ClassLoader classLoader = invokeClassLoader; classLoader != null; classLoader = classLoader.getParent()) { + String name = classLoader.getClass().getName(); + if (Objects.equals(name, bizClassLoaderName)) { + Set keys = modifiedKeysMap.computeIfAbsent(classLoader, k -> new HashSet<>()); + modifiedKeysMap.put(invokeClassLoader, keys); + return keys; + } + } + return null; + } + + private void addModifiedKey(String key) { + addModifiedKeys(Collections.singleton(key)); + } + + private void addModifiedKeys(Collection keys) { + Set modifiedKeys = getModifiedKeys(); + if (modifiedKeys != null && keys != null) { + modifiedKeys.addAll(keys); + } + } + + /** + * replace the system properties to multi biz properties
+ * if you want to use, you need invoke the method in base application + */ + public static void initSystem(String bizClassLoaderName) { + Properties properties = System.getProperties(); + MultiBizProperties multiBizProperties = new MultiBizProperties(bizClassLoaderName, + properties); + System.setProperties(multiBizProperties); + } + + public static void initSystem() { + initSystem(BIZ_CLASS_LOADER); + } +} diff --git a/sofa-serverless-runtime/sofa-serverless-common/src/test/java/com/alipay/sofa/serverless/common/util/MultiBizPropertiesTest.java b/sofa-serverless-runtime/sofa-serverless-common/src/test/java/com/alipay/sofa/serverless/common/util/MultiBizPropertiesTest.java new file mode 100644 index 000000000..a4edfe6c4 --- /dev/null +++ b/sofa-serverless-runtime/sofa-serverless-common/src/test/java/com/alipay/sofa/serverless/common/util/MultiBizPropertiesTest.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.serverless.common.util; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.*; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +public class MultiBizPropertiesTest { + private final String key1 = "test-key-1"; + private final String key2 = "test-key-2"; + + private final String key3 = "test-key-3"; + + private final String key4 = "test-key-4"; + private final String value1 = "test-value-1"; + private final String value2 = "test-value-2"; + + private final String value3 = "test-value-3"; + + private ClassLoader baseClassLoader; + + @Before + public void before() { + Thread thread = Thread.currentThread(); + baseClassLoader = thread.getContextClassLoader(); + + System.clearProperty(key1); + System.clearProperty(key2); + System.clearProperty(key3); + System.clearProperty(key4); + MultiBizProperties.initSystem(URLClassLoader.class.getName()); + } + + @After + public void after() { + Thread.currentThread().setContextClassLoader(baseClassLoader); + } + + @Test + public void testGetAndSet() { + //base: set key1=value1, base get key1=value1 + Thread thread = Thread.currentThread(); + thread.setContextClassLoader(baseClassLoader); + System.setProperty(key1, value1); + Assert.assertEquals(value1, System.getProperty(key1)); + //biz1: not set key1 value, biz1 get key1=value1 as base + ClassLoader loader1 = new URLClassLoader(new URL[0]); + thread.setContextClassLoader(loader1); + Assert.assertEquals(value1, System.getProperty(key1)); + //biz1: set key1=value2, biz1 get key1=value2 + System.setProperty(key1, value2); + Assert.assertEquals(value2, System.getProperty(key1)); + //base: still get key1=value1 + thread.setContextClassLoader(baseClassLoader); + Assert.assertEquals(value1, System.getProperty(key1)); + //biz2: not set key1 value, biz1 get key1=value1 as base + ClassLoader loader2 = new URLClassLoader(new URL[0]); + thread.setContextClassLoader(loader2); + Assert.assertEquals(value1, System.getProperty(key1)); + } + + @Test + public void testGetAndClear() { + //base: set key1=value1, base get key1=value1 + Thread thread = Thread.currentThread(); + thread.setContextClassLoader(baseClassLoader); + System.setProperty(key1, value1); + Assert.assertEquals(value1, System.getProperty(key1)); + //biz1: not set key1 value, biz1 get key1=value1 as base + ClassLoader loader1 = new URLClassLoader(new URL[0]); + thread.setContextClassLoader(loader1); + Assert.assertEquals(value1, System.getProperty(key1)); + //biz1: set key1=value2, biz1 remove key1,so biz1 get key1 is null + System.clearProperty(key1); + Assert.assertNull(System.getProperty(key1)); + //base: still get key1=value1 + thread.setContextClassLoader(baseClassLoader); + Assert.assertEquals(value1, System.getProperty(key1)); + //biz2: not set key1 value, biz1 get key1=value1 as base + ClassLoader loader2 = new URLClassLoader(new URL[0]); + thread.setContextClassLoader(loader2); + Assert.assertEquals(value1, System.getProperty(key1)); + //base: set key1=value2, base get key1=value2 + thread.setContextClassLoader(baseClassLoader); + System.setProperty(key1, value2); + Assert.assertEquals(value2, System.getProperty(key1)); + + //biz1: the key1 is removed, biz1 get key1 is null + thread.setContextClassLoader(loader1); + Assert.assertNull(System.getProperty(key1)); + + //biz2: not set key1 value, biz1 get key1=value1 as base + thread.setContextClassLoader(loader2); + Assert.assertEquals(value2, System.getProperty(key1)); + } + + @Test + public void testClone() { + Properties properties = System.getProperties(); + Properties other = (Properties) properties.clone(); + Assert.assertEquals(properties, other); + } + + @Test + public void testStoreAndLoad() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Properties properties = new MultiBizProperties(URLClassLoader.class.getName()); + properties.put(key1, value1); + properties.putAll(System.getProperties()); + int size = properties.size(); + properties.store(out, "test store"); + properties.clear(); + Assert.assertEquals(properties.size(), 0); + ByteArrayInputStream input = new ByteArrayInputStream(out.toByteArray()); + properties.load(input); + Assert.assertEquals(properties.size(), size); + + out = new ByteArrayOutputStream(); + properties.save(out, "test store"); + properties.clear(); + Assert.assertEquals(properties.size(), 0); + input = new ByteArrayInputStream(out.toByteArray()); + properties.load(input); + Assert.assertEquals(properties.size(), size); + + out = new ByteArrayOutputStream(); + properties.storeToXML(out, "test store"); + properties.clear(); + Assert.assertEquals(properties.size(), 0); + // System.out.println(out); + input = new ByteArrayInputStream(out.toByteArray()); + try { + properties.loadFromXML(input); + } catch (Throwable e) { + e.printStackTrace(); + } + + Assert.assertEquals(properties.size(), size); + Assert.assertTrue(properties.containsKey(key1)); + Assert.assertTrue(properties.contains(value1)); + + out = new ByteArrayOutputStream(); + PrintWriter writer = new PrintWriter(out); + properties.list(writer); + writer.close(); + properties.clear(); + Assert.assertTrue(properties.isEmpty()); + + input = new ByteArrayInputStream(out.toByteArray()); + Reader reader = new InputStreamReader(input); + properties.load(reader); + reader.close(); + Assert.assertFalse(properties.isEmpty()); + } + + @Test + public void testRead() { + Properties properties = new MultiBizProperties(URLClassLoader.class.getName()); + properties.setProperty(key1, value1); + Assert.assertEquals(properties.keys().nextElement(), key1); + Assert.assertTrue(properties.containsValue(value1)); + Assert.assertEquals(properties.elements().nextElement(), value1); + + AtomicInteger num = new AtomicInteger(); + properties.forEach((k, v) -> { + Assert.assertEquals(k, key1); + Assert.assertEquals(v, value1); + num.incrementAndGet(); + }); + Assert.assertEquals(num.get(), 1); + + Assert.assertEquals(properties.getOrDefault(key1, value2), value1); + Assert.assertEquals(properties.getOrDefault(key2, value2), value2); + + + Assert.assertEquals(properties.putIfAbsent(key1, value2), value1); + Assert.assertNull(properties.putIfAbsent(key2, value2)); + + Assert.assertEquals(properties.computeIfAbsent(key3, k -> value1), value1); + Assert.assertEquals(properties.computeIfPresent(key3, (k, v) -> v + value2), value1 + value2); + Assert.assertEquals(properties.computeIfPresent(key3, (k, v) -> null), null); + Assert.assertEquals(properties.computeIfPresent(key4, (k, v) -> v + value2), null); + + + properties.setProperty(key3, value1); + Assert.assertEquals(properties.compute(key3, (k, v) -> v + value2), value1 + value2); + Assert.assertEquals(properties.compute(key3, (k, v) -> null), null); + } + + @Test + public void testReplace() { + Properties properties = new MultiBizProperties(URLClassLoader.class.getName()); + properties.setProperty(key1, value1); + properties.replace(key1, value2, value3); + Assert.assertNotEquals(properties.get(key1), value3); + properties.replace(key1, value1, value3); + Assert.assertEquals(properties.get(key1), value3); + Assert.assertEquals(properties.replace(key1, value2), value3); + properties.replaceAll((k, v) -> v + value1); + Assert.assertEquals(properties.get(key1), value2 + value1); + } +} \ No newline at end of file